pythonrecursionhierarchyfamily-tree

What is a python recursion method for listing descendants from an ancestor in a non-binary tree?


I am a beginner in using Python. I have a MS Access database containing a family tree. I wish to use Python to query the family tree to list all the descendants of identified ancestors. The family tree is held in a table I have exported to excel. It has three columns: Relationship ID, Node1_ID, Node2_ID. Relationships listed in each row are uni-directional from Node1_ID to Node2_ID. The tree is non-binary, whereby the tree can have several Node2_IDs descending from a Node1_ID.

Each partner of a marriage in the position of Node1_ID relate to the same partnership node in Node2_ID (in the example below parent Nodes 1 and 2 relate to partnership Node 3). The partnership node then relates to children Nodes (e.g., partnership Node 3 connects to children Nodes 4, 5 and 6).

RID Node1_ID Node2_ID
1 1 3
2 2 3
3 3 4
4 3 5
5 3 6

I wish to use a code to call the excel spreadsheet and allow me to query a list of all descendants from a particular node through recursion. I then wish to write that list to a new excel file that I can import back into my MS Access database or to visualise in other software like GraphViz.

In searching online, I have come across the following code, which seems to do the trick:

import pandas as pd
import sys
sys.setrecursionlimit(100000000)

df = pd.read_excel(r'excelfilepath')
columns = df.columns.ravel()

def get_ancestry_dataframe_flat(df):

    def get_child_list(parent_id):

        list_of_children = list()
        list_of_children.append(df[df['P1ID'] == parent_id]['P2ID'].values)

        for i, r in df[df['P1ID'] == parent_id].iterrows():
            if r['P2ID'] != parent_id:
                list_of_children.append(get_child_list(r['P2ID']))

        # flatten list
        list_of_children = [item for sublist in list_of_children for item in sublist]
        return list_of_children

    new_df = pd.DataFrame(columns=['descendant', 'ancestor']).astype(int)
    for index, row in df.iterrows():
        temp_df = pd.DataFrame(columns=['descendant', 'ancestor'])
        temp_df['descendant'] = pd.Series(get_child_list(row['P1ID']))
        temp_df['ancestor'] = row['P1ID']
        new_df = new_df.append(temp_df)

    new_df = new_df\
        .drop_duplicates()\
        .sort_values(['ancestor', 'descendant'])\
        .reset_index(drop=True)

    return new_df

writer = pd.ExcelWriter('20210408_descendant_relationships.xlsx',engine='xlsxwriter')
get_ancestry_dataframe_flat(df).to_excel(writer,sheet_name='Sheet1')
writer.save()

The problem with this code, is that I seem to hit the recursion limit when creating a list of descendants from all ancestors (The family tree is quite big). I wish to therefore query only descendants from specific ancestors. What edits can I make to do this please? Aside from the recursion limit issue, I will still want to identify descendants from specific ancestors.

I am a beginner in Python, so I would appreciate lamens terms if possible. Thank you for your time.

EDIT: code with advice from @Ajax1234


import pandas as pd
from collections import deque

df = pd.read_excel('input.xlsx')
tree = df.values.tolist()

def get_ancestors(node):
   d, seen = deque([node]), set()
   while d:
      yield (n:=d.popleft())
      seen.add(n)
      d.extend([int(c) for _, b, c in tree if int(b) == int(n)])

r = dict([(next(n:=get_ancestors(i)), list(n)) for i in set([t[1] for t in tree])])
vals = [[[a, i] for i in b] for a, b in r.items()]
df1 = pd.DataFrame([dict(zip(['node', 'descendent'], i)) for i in vals])

print(r)

df1.to_excel('output.xlsx')

The intended output excel table should be something like:

Node Descendant
1 3
1 4
1 5
1 6
2 3
2 4
2 5
2 6
3 4
3 5
3 6

Solution

  • You can use a breadth-first search for a non-recursive solution:

    from collections import deque
    tree = [[1, 1, 3], [2, 2, 3], [3, 3, 4], [4, 3, 5], [5, 3, 6]]
    def get_ancestors(node):
       d, seen = deque([node]), set()
       while d:
          yield (n:=d.popleft())
          seen.add(n)
          d.extend([int(c) for _, b, c in tree if int(b) == int(n)])
    

    get_ancestors takes in a node and produces all descendants of the node via a breadth-first search. As an example, to get all the descendants of the node1_ids:

    r = dict([(next(n:=get_ancestors(i)), list(n)) for i in set([t[1] for t in tree])])
    

    Output:

    {1: [3, 4, 5, 6], 2: [3, 4, 5, 6], 3: [4, 5, 6]}
    

    Edit: writing to Excel:

    writer = pd.ExcelWriter('20210408_descendant_relationships.xlsx',engine='xlsxwriter')
    df = pd.read_excel(writer, 'sheet1')
    tree = df.values.tolist()
    r = dict([(next(n:=get_ancestors(i)), list(n)) for i in set([t[1] for t in tree])])
    vals = [[a, i] for a, b in r.items() for i in b]
    df1 = pd.DataFrame([dict(zip(['node', 'descendent'], i) for i in vals])
    df1.to_excel(writer)