Как преобразовать формат дерева Ньюика в древовидный иерархический объект?

1

Я хочу преобразовать файл Newick в иерархический объект (похожий на то, что было опубликовано в этом сообщении) в Python.

Мой ввод - это файл Newick, например:

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

Исходный пост анализирует строковый символ по символу. Чтобы также сохранить длины ветвей, я изменил файл JavaScript (отсюда) следующим образом:

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, чтобы также извлечь длины?

Теги:
logic
parsing
tree

3 ответа

0
Лучший ответ

Некоторые комментарии к версии JavaScript:

  • Он имеет некоторое повторение кода (if (na != '')...), что легко избежать.
  • Он использует node как имя переменной для массива. Читаемость улучшается при использовании множественного слова для массивов (или списков в Python).
  • Он не выводит то, что вы хотите иметь: он выводит узлы с именами типа "9.0: F", не выделяя длину от имени.

Из-за последней точки, код необходимо сначала исправить, прежде чем делать перевод на Python. Он должен поддерживать разделение атрибутов name/length, позволяя любому из них быть необязательным. Кроме того, он может назначать значения идентификатора каждому созданному узлу и добавлять свойство parentid для ссылки на родительский элемент.

Я лично предпочитаю кодирование с рекурсией вместо использования переменной стека. Кроме того, с помощью API регулярных выражений вы можете легко ввести токен для облегчения разбора:

JavaScript-версия парсера формата Newick

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; }

Python версия парсера формата Newick

import re

def parse(newick):
    tokens = re.findall(r"([^:;,()\s]*)(?:\s*:\s*([\d.]+)\s*)?([,);])|(\S)", newick+";")

    def recurse(nextid = 0, parentid = -1): # one node
        thisid = nextid;
        children = []

        name, length, delim, ch = tokens.pop(0)
        if ch == "(":
            while ch in "(,":
                node, ch, nextid = recurse(nextid+1, thisid)
                children.append(node)
            name, length, delim, ch = tokens.pop(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"))

О назначении node = child.children в вашем JavaScript-коде: это перемещает "указатель" (то есть node) на один уровень глубже в создаваемом дереве, так что на следующей итерации алгоритма на новый уровень добавляются новые узлы, С node = stack.pop() этот указатель отслеживает один уровень вверх в дереве.

  • 0
    @ trincot- Ваше решение идеально. Это очень элегантно Спасибо за все разъяснения тоже. Я бы даже хотел получить индекс родительского элемента для каждого узла. Например, если задано - (A: 0,1, B: 0,2, (C: 0,3, D: 0,4): 0,5), вывод должен представлять собой словарь результатов, например - result ['name'] = ['A', 'B ',' CD ',' C ',' D '], результат [' value '] = [0,1, 0,2, 0,5, 0,3, 0,4] и результат [' parent '] = [-1, -1, -1 , 2, 2] где parent содержит индекс позиции родителя узла из результата ['name']. Как можно изменить ваш код, чтобы он выводил словарь с именем, значением, родителем вместо списка? Большое спасибо!
  • 0
    Ну, это, честно говоря, далеко за пределы вопроса, который вы задали. В этом примере имя 4-го узла является производным от узлов C и D, который отличается от того, что вы указали в вопросе. Если вам нужна помощь с этим, я предлагаю вам создать новый вопрос, посвященный этому.
Показать ещё 4 комментария
0

Вот параграф синтаксического анализа для этой входной строки. Он использует pyparsing nestedExpr parser builder с определенным аргументом содержимого, так что результаты представляют собой пары пар ключ-значение, а не просто строки (которые по умолчанию).

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-float.

0

Следующий код может не быть точным переводом кода 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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню