Как мне найти родителей, содержащих собственный код в детстве? - PullRequest
3 голосов
/ 28 мая 2020

У меня есть древовидные данные, состоящие из родительских кодов, которые содержат дочерние коды, которые могут действовать как родительские, в зависимости от того, помечены ли они как «SA». Эти данные представлены в таблице Excel и выглядят следующим образом:

| Tree Level (A) | Code (B) | Spec (C) | Comm. Code (D) | Parent Code (J) |
|----------------|----------|----------|----------------|-----------------|
|              1 | A12      |        1 | SA             | Mach            |
|              2 | B41      |        2 | SA             | A12             |
|              3 | A523     |        1 | BP             | B41             |
|              2 | G32      |        4 | BP             | A12             |
|              2 | D3F5     |        1 | SA             | A12             |
|              3 | A12      |        4 | SA             | D3F5            |
|              3 | A12      |        1 | SA             | D3F5            |

Здесь есть одна проблема: A12 на верхнем уровне дерева (1) содержит дочерний элемент (D3F5), который сам содержит еще один родитель, такой же, как собственный родитель D3F5. Как вы можете себе представить, это (хотя и не представлено в данных в том виде, в каком они были доставлены мне) создает бесконечный l oop, где A12 на уровне дерева 3 снова и снова разворачивает всю структуру.

Обратите внимание, что один из двух дочерних элементов A12 не представляет проблемы, поскольку он имеет другую спецификацию относительно родительского элемента A12 на уровне дерева 1.

У меня есть функция, которая проверяет эту ситуацию, но она есть чрезвычайно медленный, так как он использует вложенные циклы для go строк, а общее количество строк может составлять несколько тысяч. Конечная цель - показать пользователю самый глубокий уровень, на котором возникает ошибка. В этом примере это будет код A12 с spe c 1 на уровне дерева 3:

def nested_parent(sht):
    """
    Checks if a parent SA contains itself as a child.
    :return: nested_parents: Dictionary of found 'nested parents'. None if none found
    """
    nested_parents = {}
    found = False

    lrow = sht.Cells(sht.Rows.Count, 1).End(3).Row
    parent_treelevel = 1

    # Get deepest tree level, as this no longer contains children
    last_treelevel = int(max([i[0] for i in sht.Range(sht.Cells(2, 1), sht.Cells(lrow, 1)).Value]))

    # Loop through parent rows
    print('Checking for nested parents...')
    for i in range(2, lrow):
        if sht.Cells(i, "D").Value == "SA":
            parent_code, parent_treelevel = f'{sht.Cells(i, "B").Value}_{sht.Cells(i, "C")}', sht.Cells(i, "A").Value

            # Add new key with list containing parent's tree level for parent code
            if parent_code not in nested_parents:
                nested_parents[parent_code] = [int(parent_treelevel)]

            # Loop child rows
            for j in range(i + 1, lrow + 1):
                child_code, child_treelevel = f'{sht.Cells(j, "B").Value}_{sht.Cells(j, "C")}', sht.Cells(i, "A").Value

                if child_code == parent_code and child_treelevel > parent_treelevel:
                    found = True
                    nested_parents[parent_code].append(int(child_treelevel))

        if parent_treelevel == last_treelevel:
            # End function if deepst tree level is reached
            print("done")
            if found:
                # Delete keys that contain no information
                delkeys = []
                for key in reversed(nested_parents):
                    if len(nested_parents[key]) == 1:
                        delkeys.append(key)
                for key in delkeys:
                    del nested_parents[key]
                return nested_parents
            else:
                return

Эту функцию можно вызвать следующим образом, где wb_name - имя книги, содержащей данные:

from win32com.client import GetObject
wb_name = "NAME"
sht = GetObject(None, "Excel.Application").Workbooks(wb_name).Worksheets(1)


def err(msg):
    """
    stops the code from executing after printing an error message
    """
    print("Unexpected error occured:", msg)
    exit()


infloop = nested_parent(sht)
if infloop is not None:
    dict_str = ''.join([f'Code: {key}, Tree levels: {infloop[key]}\n' for key in infloop])
    err(f"Warning: one or more parent codes contain their own code as a child:\n{dict_str}")

Я надеюсь ускорить этот код, так как остальная часть моего скрипта выполняется довольно быстро, и его скорость серьезно ограничивается этой функцией.

Ответы [ 2 ]

3 голосов
/ 07 июня 2020

Как упоминалось @ a'r, ваши «древовидные данные» можно рассматривать как ориентированный граф, то есть точки (узлы), соединенные стрелками (ориентированные ребра). Существует очень мощная библиотека под названием networkx, которая очень хорошо работает с графиками. Не углубляясь в теорию графов, рассмотрим следующий пример кода:

import networkx as nx

edges = [ ('A12', 'Mach'), 
          ('B41', 'A12'),
          ('A523','B41'),
          ('G32', 'A12'),
          ('D3F5','A12'),
          ('A12', 'D3F5'),
          ('A12', 'D3F5') ]

G = nx.DiGraph(edges)
cycles_list = list(nx.simple_cycles(G))
print(cycles_list)

Вывод:

[['A12', 'D3F5']]

Здесь имена узлов представляют собой коды, когда вы их читаете, а ребра - это соединения. между ребенком и родителем. Вы можете легко создать список ребер, просто взяв соответствующие столбцы вашего файла Excel. Точное направление (родитель к ребенку или наоборот) в этом случае не очень важно, просто оставайтесь последовательными.

simple_cycles возвращает генератор. Здесь вы можете найти документацию по нему.

обновление

После того, как у вас есть список циклов, чтобы найти самый глубокий узел, вам нужно сопоставить узел и найти его наиболее глубокий вид.

Создайте список ваших узлов из столбцов A, B и J. Он будет выглядеть так:

data = [
   [1, 'A12', 'Mach'],
   [2, 'B41', 'A12'],
   [3, 'A523', 'B41'],
   [2, 'G32', 'A12'],
   [2, 'D3F5', 'A12'],
   [3, 'A12', 'D3F5'],
   [3, 'A12', 'D3F5'] ]

result = {}
for entry in data:
    for el in cycles_list:
        if entry[1:] == el:
            key = tuple(el)
            result[key] = max(result.setdefault(key, 0), entry[0])
print(result)

>>>
{('A12', 'D3F5'): 3}

Теперь вы получите словарь, где ключом является проблемный узел c и value - это самый глубокий уровень, на котором его можно найти.

1 голос
/ 05 июня 2020

Я надеюсь, что этот ответ поможет продемонстрировать мощь иерархической структуры данных. Я переписал данные в виде строки json, а затем написал код для обхода иерархии и создания отчета. У вас по-прежнему будет задача преобразовать excel в json. Суть в том, что каждый уровень json имеет одни и те же ключи, и что каждый дочерний элемент в дочерних имеет те же ключи, что и его родительский словарь, что позволяет рекурсивной функции перемещаться по структуре. Я делал примеры для суммирования по кодам или уровням.

import json 
json_data = """
{
    "level": 0,
    "code": "Mach",
    "children": [
        {
            "level": 1,
            "code": "A12",
            "children": [
                {
                    "level": 2,
                    "code": "B41",
                    "children": [
                        {
                            "level": 3,
                            "code": "A523",
                            "children": []
                        }
                    ]
                },
                {
                    "level": 2,
                    "code": "G32",
                    "children": []
                },
                {
                    "level": 2,
                    "code": "D3F5",
                    "children": [
                        {
                            "level": 3,
                            "code": "A12",
                            "children": []
                        },
                        {
                            "level": 3,
                            "code": "A12",
                            "children": []
                        }
                    ]
                }
            ]
        }
    ]
}
"""

data = json.loads(json_data)

def crawl_levels(mydict, result={}):
    try:
        result[mydict["level"]].append(mydict["code"])
    except:
        result[mydict["level"]] = [mydict["code"],]

    for i in mydict["children"]:
        result = crawl_levels(i, result=result)
    return result

crawl_levels(data) 
>>>{0: ['Mach'], 1: ['A12'], 2: ['B41', 'G32', 'D3F5'], 3: ['A523', 'A12', 'A12']}

def crawl_codes(mydict, result={}):
    try:
        result[mydict["code"]].append(mydict["level"])
    except:
        result[mydict["code"]] = [mydict["level"],]

    for i in mydict["children"]:
        result = crawl_codes(i, result=result)
    return result

crawl_codes(data) 
>>>{'Mach': [0],
 'A12': [1, 3, 3],
 'B41': [2],
 'A523': [3],
 'G32': [2],
 'D3F5': [2]}
...