Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Classify functions #19

Merged
merged 37 commits into from
Nov 23, 2017
Merged

Classify functions #19

merged 37 commits into from
Nov 23, 2017

Conversation

sjanssen2
Copy link
Collaborator

Addressing #18 , I here created to QIIME 2 functions "classify-otus" and "classify-paths" for two alternative strategies to assign taxonomic labels to inserted fragments.

@sjanssen2
Copy link
Collaborator Author

I split the functionality of the plugin into "sepp" and "classify-x", to enable providing alternatives for "x", see issue #19
@wasade @antgonza could you also review this one please?

Copy link
Member

@antgonza antgonza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to submit review, sorry.

filename_default_taxonomy).view(pd.DataFrame)

# ensure feature IDs are strings to match IDs from the tree
reference_taxonomy.index = map(str, reference_taxonomy.index)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this raise a nice error if they don't match? Also, could you expand/explain a bit more what's the expectation in the comments? Thanks

reference_taxonomy.index = map(str, reference_taxonomy.index)

# load the insertion tree
tree = skbio.TreeNode.read(str(tree))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be better to use to_newick or something like that than converting to str, here you are assuming some specific behavior that might change, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tree is a QIIME2 type NewickFormat, which is inherited from model.TextFileFormat, thus it is some kind of a file and with str() I obtain the file name.
I don't see definition of "to_newick" in this type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks. I thought it was something different, perhaps adding docstrings will help with this kind of possible confusions.

names_fragments = set([fragment.metadata['id']
for fragment
in representative_sequences.file.view(DNAIterator)])
if len((set(names_tips) - set(names_fragments)) -
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously, this is a nice and concise error but not sure if in some cases, perhaps when verbose is on, it will be good to display al the discrepancies.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. How do I check if verbose is set?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, I think this is a good question for the Q2 forum ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got help from slack and am now reporting on stderr which feature IDs are missing. User can see that if verbose is set.

taxonomy = []
for fragment in representative_sequences.file.view(DNAIterator):
lineage_str = np.nan
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a quite long try/except block, is it possible to narrow what it covers?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It covers if line 212 cannot find fragment fragment.metadata['id'] in the tree object. I don't know if my code is cleaner if I have a small try except block around that line and than enclose the lengthy block with an additional if condition?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I thought and I wasn't sure what was more pythonic. Anyway, found this and I think it makes sense: https://stackoverflow.com/a/1835844. So perhaps should do the trick:

try:
   curr_node = tree.find(fragment.metadata['id'])
   foundOTUs = []
except:
   foundOTUs = [False]

BTW, just realized if len(foundOTUs) > 0: is always true once the while is done.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foundOTUs should be the empty list if the whole tree has been traversed without finding any suitable OTU. The while loop can be exited via break if root has been reached

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figure if you are confused by the long try except block, I should shorten it. Now it surrounds just one line and I am using an additional if statement

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized what was my confusion, thanks for your patience. The condition is the break. What about moving the code in the if len(foundOTUs) > 0: to the if curr_node.is_root(): and then break.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds possible, but I would prefer to leave as is, since I find that more understandable. But I can change if you strongly disagree. Let me know.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scrap my last statement! That would mean we always traverse all the way up to the root and collect ALL OTU labels in the three. Later we would find the longest common prefix if ALL labels, which is "" and thus would render this function useless. We cannot change as you suggested.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the body of the while be decomposed? It could help interpretation and reduce complexity

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am lost. As the writer of this piece of code, I find it very readable, but obviously that is not true for anyone else :-/
I am open for concrete suggestions on how to change the code, but I don't know which direction I should take here.
It is a rather complex concept on how to traverse the tree and there are some edge cases to consider.
I now added some 30 lines of comments to explain my code. Does that help?

@sjanssen2
Copy link
Collaborator Author

@wasade could you provide a second review?

@sjanssen2
Copy link
Collaborator Author

Hey @qiyunzhu, do you have some capacity to review? Thanks!

Copy link
Member

@wasade wasade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if comments are short, on phpne

README.md Outdated

You can then use `insertion-tree.qza` for all downstream analyses, e.g. "Alpha and beta diversity analysis", instead of `rooted-tree.qza`.

### Assign taxonomy

The *fragment-insertion* plugin provides two alternative methods to assign a taxonomic lineage to every fragment. Assume the tips of your reference phylogeny are e.g. OTU-IDs from Greengenes (which is the case when you use the default reference). If you have a taxonomic mapping for every OTU-ID to a lineage string, as provided by Greengenes, function `classify-otus` will detect the closest OTU-IDs for every fragment in the insertion tree and report this OTU-IDs lineage string for the fragment. Thus, the function expects two required input artifacts: 1) the representative-sequences of type `FeatureData[Sequence]` and 2) the resulting tree of a previous `sepp` run which is of type `Phylogeny[Rooted]`. For the example, we also specify a third, optional input [taxonomy_gg99.gza](https://raw.githubusercontent.com/biocore/q2-fragment-insertion/master/taxonomy_gg99.gza) of type `FeatureData[Taxonomy]`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is a .gza?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a stupid typo which I cannot get out of my muscle memory

README.md Outdated
qiime fragment-insertion classify-otus \
--i-representative-sequences rep-seqs.qza \
--i-tree insertion-tree.qza \
--i-reference-taxonomy taxonomy_gg99.gza \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

README.md Outdated

You need to make sure, that the `--i-reference-taxonomy` matches the reference phylogeny used with function `sepp`.

Alternatively, you can use the function `classify-paths` to a taxonomy. The lineage strings are obtained by traversing the insertion tree from each fragment tip towards the root and collecting all taxonomic labels in the reference tree along this path. Thus, taxonomy is only as good as provided reference phylogeny. Note, taxonomic labels are identified by containing two underscore characters `_` `_` as in Greengenes. **As of Nov 2017: We do NOT encourage the use of this function, since it has not been compared to existing taxonomic assignment methods. Particularly since the default reference tree is not inline with the reference taxonomy.**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do not recommend its use, can be remove it until it has been evaluated?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or more explicitly note the method as experimental? e.g., classify-paths-experimental? This point "Particularly since the default reference tree is not inline with the reference taxonomy." I think just serves to confuse a user, simply saying it's experimental should be fine

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree and I think the cleanest solution for now is to comment the code out and remove description from Readme.md, which I did. I don't want the code to get lost, therefore I did not remove according lines, but chose to comment them out.

README.md Outdated
--input-path taxonomy.tsv \
--source-format HeaderlessTSVTaxonomyFormat \
--type "FeatureData[Taxonomy]" \
--output-path foo.gza
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. Was a note to myself. I added some explanatory text why one want to do that.


# ensure that all reference tips in the tree (those without the inserted
# fragments) have a mapping in the user provided taxonomy table
names_tips = set([node.name for node in tree.tips()])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set comprehension? {x for x in thing}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, wasn't aware of this. Thanks!

names_fragments = set([fragment.metadata['id']
for fragment
in representative_sequences.file.view(DNAIterator)])
missing_features = (set(names_tips) - set(names_fragments)) -\
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extraneous casts

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

try:
curr_node = tree.find(fragment.metadata['id'])
except skbio.tree.MissingNodeError:
curr_node = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just continue?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea

if curr_node is not None:
foundOTUs = []
while len(foundOTUs) == 0:
for node in curr_node.postorder():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repeated calls to traverse subtrees is quite expensive, why not just walk .ancestors?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

due to the topology imposed by the fragment insertion we cannot rely the relationship between the found node and the closest annotated OTU tip. Therefore, we need to really investigate the full sub-tree. Unfortunately, this is expensive, but I don't see a shortcut.

Keep in mind that we do a bottom up traversal and most fragments should be inserted very close to the next OTU tip, thus I think on average this procedure is not that bad.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could lead to a spectacularly bad assignment though for reads from candidate phyla that get inserted deep into the tree, right? Safest assignment is ancestors, suggest only investigating cousins and descendents if there is a rational branch length threshold in place which i think will be hard to fit well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the following example:
ex
How would you limit the search in the tree for fragment "ins1_toD+E" ? Strategy of "classify-otus" is to look down to the tips, not up to the ancestors. That is the strategy of "classify-paths".

@wasade
Copy link
Member

wasade commented Nov 22, 2017 via email

@sjanssen2
Copy link
Collaborator Author

It is a hybrid. First, trying to find the closest OTUs by traversing the tree from the insertion tip node bottom up until one or several equally good OTU tips are found. For all found OTUs we look up the lineage string and return the longest common prefix of the lineage strings. Note, we don't operate on single characters but on rank names, i.e. all separated by "; ".

It is not benchmarkes and I think a benchmark of taxonomic assignment performance would take a couple further weeks if not months. But I really like to push this plugin out soon. We could mark this function as experimental if you like

@wasade
Copy link
Member

wasade commented Nov 22, 2017 via email

@sjanssen2
Copy link
Collaborator Author

OK. Do you happen to know how I properly flag this function as being experimental?

@wasade
Copy link
Member

wasade commented Nov 22, 2017

Change the method name so it has experimental in it? What I think would be helpful is for a QIIME2 user on the command line to have to type qiime fragment-insertion classify-otus-experimental ... to proceed, does that make sense?

@sjanssen2
Copy link
Collaborator Author

@wasade @antgonza OK now?

@sjanssen2
Copy link
Collaborator Author

ping @wasade

@@ -79,4 +74,70 @@
)


# text for readme, if we decide to put function 'classify_paths' back in:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these comments just be deleted?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean the whole block of out-commented code or just one specific line?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wasade another ping :-)

@wasade
Copy link
Member

wasade commented Nov 22, 2017

One comment, 👍 otherwise

@sjanssen2 sjanssen2 merged commit 2c59d6b into qiime2:master Nov 23, 2017
@sjanssen2 sjanssen2 deleted the classify-functions branch February 20, 2018 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants