【问题标题】:How to convert Newick tree format to a tree-like hierarchical object?如何将 Newick 树格式转换为树状分层对象?
【发布时间】:2018-07-17 04:37:08
【问题描述】:

我想在 Python 中将 Newick file 转换为分层对象(类似于 this post 中发布的内容)。

我的输入是这样的 Newick 文件:

(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F:0.9

原帖逐个字符解析字符串。为了也存储分支长度,我修改了 JavaScript 文件(来自here),如下所示:

var newick = '// (A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F:0.9',
    stack = [],
    child,
    root = [],
    node = root;

var na = "";
newick.split('').reverse().forEach(function(n) {
    switch(n) {
    case ')':
        // ')' => begin child node
        if (na != "") {
            node.push(child = { name: na });
            na = "";
        }
        stack.push(node);
        child.children = [];
        node = child.children;
        break;

    case '(':
        // '(' => end of child node
        if (na != "") {
            node.push(child = { name: na });
            na = "";
        }
        node = stack.pop();
        // console.log(node);
        break;

    case ',':
        // ',' => separator (ignored)
        if (na != "") {
            node.push(child = { name: na });
            na = "";
        }
        break;

    default:
        // assume all other characters are node names
        // node.push(child = { name: n });
        na += n;
        break;
    }
});

console.log(node);

现在,我想把这段代码翻译成 Python。

这是我的尝试(我知道这是不正确的):

class Node:

  def __init__(self):
    self.Name = ""
    self.Value = 0
    self.Children = []

newick = "(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"
stack = []
# root = []
# node = []

for i in list(reversed(newick)):
  if i == ')':
    if na != "":
      node = Node()
      node.Name = na
      child.append(node)
      na = ""
    stack.append(node)
    # insert logic
    child = node.Children
    # child.append(child)

  elif i == '(':
    if (na != ""):
      child = Node()
      child.Name = na
      node.append(child)
      na = ""
    node = stack.pop()
  elif i == ',':
    if (na != ""):
      node = Node()
      node.Name = na
      node.append(child)
      na = ""
  else:
    na += n

由于我对 JavaScript 完全陌生,因此无法将代码“翻译”成 Python。特别是,我不明白以下几行:

child.children = [];
node = child.children;

如何在 Python 中正确编写此代码,同时提取长度?

【问题讨论】:

    标签: javascript python parsing tree logic


    【解决方案1】:

    JavaScript 版本上的一些 cmets:

    • 它有一些很容易避免的代码重复 (if (na != '') ...)。
    • 它使用node 作为数组的变量名。当您对数组(或 Python 中的列表)使用复数词时,可读性会提高。
    • 它不输出您想要的:它输出名称如“9.0:F”的节点,而不是从名称中分离长度。

    由于最后一点,在翻译成 Python 之前,首先需要更正代码。它应该支持拆分名称/长度属性,允许它们中的任何一个是可选的。此外,它可以为每个创建的节点分配 id 值,并添加一个 parentid 属性来引用节点的父节点。

    我个人更喜欢使用递归而不是使用堆栈变量进行编码。此外,使用正则表达式 API,您可以轻松标记输入以促进解析:

    Newick 格式解析器的 JavaScript 版本

    function parse(newick) {
        let nextid = 0;
        const regex = /([^:;,()\s]*)(?:\s*:\s*([\d.]+)\s*)?([,);])|(\S)/g;
        newick += ";"
        
        return (function recurse(parentid = -1) {
            const children = [];
            let name, length, delim, ch, all, id = nextid++;;
    
            [all, name, length, delim, ch] = regex.exec(newick);
            if (ch == "(") {
                while ("(,".includes(ch)) {
                    [node, ch] = recurse(id);
                    children.push(node);
                }
                [all, name, length, delim, ch] = regex.exec(newick);
            }
            return [{id, name, length: +length, parentid, children}, delim];
        })()[0];
    }
    
    // Example use:
    console.log(parse("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"));
    .as-console-wrapper { max-height: 100% !important; top: 0; }

    Newick 格式解析器的 Python 版本

    import re
    
    def parse(newick):
        tokens = re.finditer(r"([^:;,()\s]*)(?:\s*:\s*([\d.]+)\s*)?([,);])|(\S)", newick+";")
    
        def recurse(nextid = 0, parentid = -1): # one node
            thisid = nextid;
            children = []
    
            name, length, delim, ch = next(tokens).groups(0)
            if ch == "(":
                while ch in "(,":
                    node, ch, nextid = recurse(nextid+1, thisid)
                    children.append(node)
                name, length, delim, ch = next(tokens).groups(0)
            return {"id": thisid, "name": name, "length": float(length) if length else None, 
                    "parentid": parentid, "children": children}, delim, nextid
    
        return recurse()[0]
    
    # Example use:
    print(parse("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"))
    

    关于 JavaScript 代码中的赋值 node = child.children:这会将“指针”(即 node)移动到正在创建的树中更深一层,以便在算法的下一次迭代中添加任何新节点在那个级别。使用node = stack.pop(),该指针在树中向上追溯一级。

    【讨论】:

    • @trincot- 您的解决方案非常完美。它非常优雅。也感谢所有的澄清。我什至想获得每个节点的孩子父母的索引。例如,给定 - (A:0.1,B:0.2,(C:0.3,D:0.4):0.5),输出应该是结果字典,例如 - result['name'] = ['A', 'B ', 'CD', 'C', 'D'], result['value'] = [0.1, 0.2, 0.5, 0.3, 0.4] 和 result['parent'] = [-1, -1, -1 , 2, 2] 其中 parent 包含来自 result['name'] 的节点父级位置的索引。如何修改您的代码,使其输出带有名称、值、父级而不是列表的字典?非常感谢!
    • 嗯,老实说,这远远超出了您提出的问题。在此示例中,第 4 个节点的名称是节点 C 和 D 的派生,这与您在问题中输入的名称不同。如果您需要这方面的帮助,我建议您为此创建一个新问题。
    • 好吧,无论如何我都想这样做:我用解析任何 Newick 兼容字符串的代码更新了我的答案,并向每个节点添加了 parentidchildren 属性。享受吧!
    • @trincot- 非常感谢所有的帮助。您当前的 javascript 代码提供了我需要的确切输出(一个小错误 - 它无法为根元素提供名称和长度值)。但是,我需要在 python 中实现它。 python 版本无法提供正确的树结构。我试过的字符串在这里-gist.github.com/chaityacshah/f79f36bd48937663762191fa82e93d10。我一直在尝试修改您的代码,但一直无法这样做。你能帮忙提供正确的python版本吗?再次感谢!
    • 更正了。 JS版本没有加“;”如果缺少分隔符,则在末尾添加分隔符(据我了解,newick 格式需要它)。 Python 版本有一个pop(),它应该是pop(0)
    【解决方案2】:

    以下代码可能不是 javascript 代码的精确翻译,但它可以按预期工作。有一些像“n”这样的问题没有定义。我还添加了对节点名称的解析为名称和值,以及一个父字段。

    您应该考虑使用已经存在的解析器,例如 https://biopython.org/wiki/Phylo,因为它们已经为您提供了使用树的基础架构和算法。

    class Node:
        # Added parsing of the "na" variable to name and value.
        # Added a parent field
        def __init__(self, name_val):
            name, val_str = name_val[::-1].split(":")
            self.name = name
            self.value = float(val_str)
            self.children = []
            self.parent = None
    
        # Method to get the depth of the node (for printing)
        def get_depth(self):
            current_node = self
            depth = 0
            while current_node.parent:
                current_node = current_node.parent
                depth += 1
            return depth
    
        # String representation
        def __str__(self):
            return "{}:{}".format(self.name, self.value)
    
    newick = "(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5,G:0.8)F:0.9"
    
    root = None
    # na was not defined before.
    na = ""
    stack = []
    for i in list(reversed(newick)):
        if i == ')':
            if na != "":
                node = Node(na)
                na = ""
                if len(stack):
                    stack[-1].children.append(node)
                    node.parent = stack[-1]
                else:
                    root = node
                stack.append(node)
    
        elif i == '(':
            if (na != ""):
                node = Node(na)
                na = ""
                stack[-1].children.append(node)
                node.parent = stack[-1]
            stack.pop()
        elif i == ',':
            if (na != ""):
                node = Node(na)
                na = ""
                stack[-1].children.append(node)
                node.parent = stack[-1]
        else:
            # n was not defined before, changed to i.
            na += i
    
    # Just to print the parsed tree.
    print_stack = [root]
    while len(print_stack):
        node = print_stack.pop()
        print(" " * node.get_depth(), node)
        print_stack.extend(node.children)
    

    最后打印位的输出如下:

     F:0.9
      A:0.1
      B:0.2
      E:0.5
       C:0.3
       D:0.4
      G:0.8
    

    【讨论】:

      【解决方案3】:

      这里是这个输入字符串的 pyparsing 解析器。它使用 pyparsing 的 nestedExpr 解析器构建器,并带有定义的内容参数,因此结果是解析的键值对,而不仅仅是简单的字符串(这是默认值)。

      import pyparsing as pp
      # suppress punctuation literals from parsed output
      pp.ParserElement.inlineLiteralsUsing(pp.Suppress)
      
      ident = pp.Word(pp.alphas)
      value = pp.pyparsing_common.real
      
      element = pp.Group(ident + ':' + value)
      parser = pp.OneOrMore(pp.nestedExpr(content=pp.delimitedList(element) + pp.Optional(','))
                            | pp.delimitedList(element))
      
      tests = """
          (A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F:0.9
      """
      parsed_results = parser.parseString(tests)
      import pprint
      pprint.pprint(parsed_results.asList(), width=20)
      

      给予:

      [[['A', 0.1],
        ['B', 0.2],
        [['C', 0.3],
         ['D', 0.4]],
        ['E', 0.5]],
       ['F', 0.9]]
      

      请注意,用于解析实数的 pyparsing 表达式也会在解析时转换为 Python 浮点数。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-03-29
        • 1970-01-01
        • 2013-02-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-08-21
        • 1970-01-01
        相关资源
        最近更新 更多