LoginSignup
1
2

【光る君へ】藤原道長の家系図を Treelib で書く

Posted at

おはこんばんちわ。今日も引き続きChatGPT先生をお迎えして、「ChatGPTとPythonで学ぶ Treelib」というテーマで雑談したいと思います。それではChatGPT先生、よろしくお願いします。


assistant:

よろしくお願いします。PythonのTreelibライブラリは、効率的なツリー構造をPythonで実装するためのパワフルな公開ライブラリで、シンプルに使用することができます。ツリー構造データは、組織や辞書、ファミリーツリー、機械学習アルゴリズム等、様々な場面で重要な役割を果たします。

最初にチュートリアルを始める前に、ライブラリをインストールする必要があります。ターミナル、またはパワーシェルを開き、次のコマンドを入力します:

pip install treelib

それでは、基本的な使い方から始めてみましょう!


user:

ありがとうございます。Treelibを使って、次の家系図を取り扱うPythonコードを書いてください。

藤原師輔
├── 藤原兼家
│   ├── 藤原超子
│   ├── 藤原道兼
│   ├── 藤原道長
│   └── 藤原道隆
└── 藤原安子
    ├── 円融天皇
    └── 冷泉天皇

assistant:

Treelibを使って上記の家系図を扱うPythonコードは以下の通りです:

from treelib import Node, Tree

# Treeオブジェクトの生成
family_tree = Tree()

# ノードの追加
family_tree.create_node("藤原師輔", "藤原師輔")  # root node

family_tree.create_node("藤原兼家", "藤原兼家", parent="藤原師輔")
family_tree.create_node("藤原超子", "藤原超子", parent="藤原兼家")
family_tree.create_node("藤原道兼", "藤原道兼", parent="藤原兼家")
family_tree.create_node("藤原道長", "藤原道長", parent="藤原兼家")
family_tree.create_node("藤原道隆", "藤原道隆", parent="藤原兼家")

family_tree.create_node("藤原安子", "藤原安子", parent="藤原師輔")
family_tree.create_node("円融天皇", "円融天皇", parent="藤原安子")
family_tree.create_node("冷泉天皇", "冷泉天皇", parent="藤原安子")

# 生成した家系図の表示
family_tree.show()

このコードではまず、TreeNodeをimportします。次に、Treeオブジェクトfamily_treeを作成します。その後、rootノードとして"藤原師輔"を作ります。ここで、第1引数はノードの名称、第2引数はノードの識別子(一意でなければなりません)、第3引数parentは親ノードの識別子を指定します。

その後、残りの人々のノードも同様に作成します。ノードの作成が終了したら、family_tree.show()を呼び出すことで家系図を表示します。

このコードを走らせると、指定したツリー構造がコンソール上に表示されます。


user:

ところがどっこい、結果はこうなりました。

b'\xe8\x97\xa4\xe5\x8e\x9f\xe5\xb8\xab\xe8\xbc\x94\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 \xe8\x97\xa4\xe5\x8e\x9f\xe5\x85\xbc\xe5\xae\xb6\n\xe2\x94\x82   \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 \xe8\x97\xa4\xe5\x8e\x9f\xe8\xb6\x85\xe5\xad\x90\n\xe2\x94\x82   \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 \xe8\x97\xa4\xe5\x8e\x9f\xe9\x81\x93\xe5\x85\xbc\n\xe2\x94\x82   \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 \xe8\x97\xa4\xe5\x8e\x9f\xe9\x81\x93\xe9\x95\xb7\n\xe2\x94\x82   \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 \xe8\x97\xa4\xe5\x8e\x9f\xe9\x81\x93\xe9\x9a\x86\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 \xe8\x97\xa4\xe5\x8e\x9f\xe5\xae\x89\xe5\xad\x90\n    \xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 \xe5\x86\x86\xe8\x9e\x8d\xe5\xa4\xa9\xe7\x9a\x87\n    \xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 \xe5\x86\xb7\xe6\xb3\x89\xe5\xa4\xa9\xe7\x9a\x87\n'

assistant:

ご不便をお掛けしてすみません。この問題はPythonで非ASCII文字を扱う際にエンコーディング相違によく起こるもので、Unix系のシステム(LinuxやMac)では通常このような問題は起こらないのですが、Windowsではこのような状況が発生することがあります。

Treelibは内部で文字列を扱うためにos.linesepを使用するため、改行コードの相違がこの問題を引き起こしています。Windowsでは改行コードが「\r\n」であり、Unix系のシステムでは「\n」が使用されます。

幸いなことに、解決策は簡単です。treelibのメソッドshow()が戻す文字列をそのままprint関数で出力すれば解決します。以下のようにコードを修正してみてください:

from treelib import Node, Tree

# Treeオブジェクトの生成
family_tree = Tree()

# ノードの追加
family_tree.create_node("藤原師輔", "藤原師輔")  # root node

family_tree.create_node("藤原兼家", "藤原兼家", parent="藤原師輔")
family_tree.create_node("藤原超子", "藤原超子", parent="藤原兼家")
family_tree.create_node("藤原道兼", "藤原道兼", parent="藤原兼家")
family_tree.create_node("藤原道長", "藤原道長", parent="藤原兼家")
family_tree.create_node("藤原道隆", "藤原道隆", parent="藤原兼家")

family_tree.create_node("藤原安子", "藤原安子", parent="藤原師輔")
family_tree.create_node("円融天皇", "円融天皇", parent="藤原安子")
family_tree.create_node("冷泉天皇", "冷泉天皇", parent="藤原安子")

# 生成した家系図の表示
print(family_tree.show())

この修正により、家系図が正しく表示されるはずです。


user:

また同じ結果ですね。解決してないです。

print(family_tree.show(stdout=False))

上記のようにすると、次のように求める結果が得られました。

藤原師輔
├── 藤原兼家
│   ├── 藤原超子
│   ├── 藤原道兼
│   ├── 藤原道長
│   └── 藤原道隆
└── 藤原安子
    ├── 円融天皇
    └── 冷泉天皇

assistant:

申し訳ありません、ご指摘の通りです。

デフォルトではtree.show()は標準出力に出力しますが、引数stdout=Falseを指定することで、文字列として結果を返すことができます。その結果を直接print関数で出力することで、期待した結果が得られます。

ご自身で解決策を見つけてくださり、ありがとうございます。これは重要な情報ですので、他の読者にもご共有いただければと思います。

以下のようにコードを修正することで正しく結果が出力されます:

from treelib import Node, Tree

# Treeオブジェクトの生成
family_tree = Tree()

# ノードの追加
family_tree.create_node("藤原師輔", "藤原師輔")  # root node

family_tree.create_node("藤原兼家", "藤原兼家", parent="藤原師輔")
family_tree.create_node("藤原超子", "藤原超子", parent="藤原兼家")
family_tree.create_node("藤原道兼", "藤原道兼", parent="藤原兼家")
family_tree.create_node("藤原道長", "藤原道長", parent="藤原兼家")
family_tree.create_node("藤原道隆", "藤原道隆", parent="藤原兼家")

family_tree.create_node("藤原安子", "藤原安子", parent="藤原師輔")
family_tree.create_node("円融天皇", "円融天皇", parent="藤原安子")
family_tree.create_node("冷泉天皇", "冷泉天皇", parent="藤原安子")

# 生成した家系図の表示
print(family_tree.show(stdout=False))

この修正により、家系図が正しく表示されます。


user:

family_tree.create_node の第1引数と第2引数が同じに見えますが、どういうことか説明していただけますか?


assistant:

もちろんです。

create_nodeメソッドは一つのノードをツリーに追加しますが、その引数は以下の通りです:

  1. 第1引数tagはノードの表示名(ユーザが見る名前)を指定します。これはツリーを表示した際にノードとして表示させる名前です。
  2. 第2引数identifierはそのノードを一意に特定するためのIDを指定します。これは内部的にノードを特定するために使用され、通常はユーザには表示されません。

なので、この2つの引数の値が同じであることは特に問題ありません。

これらが同じ場合と異なる場合の例を挙げます。

  1. 同じ場合:

    • ファミリーツリー、組織図など、ノードの名前が一意である(すなわち、同じ名前を持つノードが2つ存在しない)場合、tagidentifierは同じにしても問題ありません。先程の家系図コード例が該当します。
  2. 異なる場合:

    • しかし、ノードの名前が一意でない場合(例えば、クラス全体の生徒さんをノードとした木を作る場合など)、一つの名前に対して複数のIDが必要となるかもしれません。そのような場合、tagidentifierは異なる値にします。例えば、tagには生徒の名前を設定し、identifierには一意の生徒IDを設定することができます。

user:

ありがとうございます。それでは、上記のコードをベースに、家系図を階層的なJSON形式ファイルに変換して tree.json という名前で保存するPythonコードを書いてください。


assistant:

treelibライブラリは直接のJSONエクスポート機能を提供していませんが、ツリーを辞書に変換してから、それをJSONに変換することが可能です。これはPythonのjsonモジュールを使用して行います。

以下のコードでそれを行うことができます:

import json
from treelib import Node, Tree

# Treeオブジェクトの生成
family_tree = Tree()

# ノードの追加
family_tree.create_node("藤原師輔", "藤原師輔")  # root node

family_tree.create_node("藤原兼家", "藤原兼家", parent="藤原師輔")
family_tree.create_node("藤原超子", "藤原超子", parent="藤原兼家")
family_tree.create_node("藤原道兼", "藤原道兼", parent="藤原兼家")
family_tree.create_node("藤原道長", "藤原道長", parent="藤原兼家")
family_tree.create_node("藤原道隆", "藤原道隆", parent="藤原兼家")

family_tree.create_node("藤原安子", "藤原安子", parent="藤原師輔")
family_tree.create_node("円融天皇", "円融天皇", parent="藤原安子")
family_tree.create_node("冷泉天皇", "冷泉天皇", parent="藤原安子")

# ツリーを辞書に変換
tree_dict = family_tree.to_dict(with_data=True)

# JSON形式に変換と保存
with open('tree.json', 'w') as f:
    json.dump(tree_dict, f, ensure_ascii=False, indent=4)

このコードは、まずfamily_treeを辞書に変換します(to_dict()メソッドを利用)。そして、json.dumpを使ってこの辞書をJSON形式としてファイルに書き出します。ensure_ascii=Falseは非ASCII文字もエンコードするための設定で、indent=4はJSONの整形(インデント)のための設定です。


user:

ありがとうございます。できたファイルを

cat tree.json

したところ、ファイルの内容を次のように確認できました。

{
    "藤原師輔": {
        "children": [
            {
                "藤原兼家": {
                    "children": [
                        {
                            "藤原超子": {
                                "data": null
                            }
                        },
                        {
                            "藤原道兼": {
                                "data": null
                            }
                        },
                        {
                            "藤原道長": {
                                "data": null
                            }
                        },
                        {
                            "藤原道隆": {
                                "data": null
                            }
                        }
                    ],
                    "data": null
                }
            },
            {
                "藤原安子": {
                    "children": [
                        {
                            "円融天皇": {
                                "data": null
                            }
                        },
                        {
                            "冷泉天皇": {
                                "data": null
                            }
                        }
                    ],
                    "data": null
                }
            }
        ],
        "data": null
    }
}

assistant:

はい、おっしゃる通りです。treelibto_dict()メソッドを利用すると、ツリー構造を階層的な辞書形式(すなわちネストされた辞書)で表現します。辞書のキーはノード名(tag)で、その値はノードのデータ部分と子ノードのリストを含む別の辞書として表現されます。

"data": null部分は、各ノードのdata属性を表します。create_nodeメソッドを呼び出す際にdata引数を指定しなかったため、ここはnullとなっています。data属性を利用すると、各ノードに付加的なデータ(例えばノード特有の属性情報など)を格納することができます。

家系図をJSON形式で表現した結果が正しく得られていることがわかりますので、これでPythonからのJSON出力が成功したことになります。これを確認するためにcatコマンドをお使いいただき、ありがとうございます。


user:

ありがとうございます。それでは、さきほどの tree.json を読み込んで、treelib で取り扱える形式に変換し、print(family_tree.show(stdout=False)) で表示するPythonコードを書いてください。


assistant:

そのためには、まず辞書からツリーへ再帰的にノードを生成する関数が必要です。ファイルからJSONデータを読み込み、ツリーを作成し、表示するPythonコードは次のとおりです:

import json
from treelib import Node, Tree

def add_node(n, parent = None):
    name = list(n.keys())[0]
    tree.create_node(name, name, parent = parent)
    children = n[name].get('children', [])
    for child in children:
        add_node(child, parent = name)

# JSON形式データの読み込み
with open('tree.json', 'r') as f:
    data = json.load(f)

# Treeオブジェクトの生成
tree = Tree()

# ツリーの作成
add_node(data)

# 生成した家系図の表示
print(tree.show(stdout=False))

このコードでは、add_node関数を定義しています。この関数は、与えられた辞書からノードを作成し、その子ノードが存在すれば再帰的にその子ノードを作成します。そして、読み取ったJSONデータのルートノードからこのadd_node関数を呼び出すことでツリーを作成します。


user:

ありがとうございます。動作確認しました。それでは、次の家系図を階層的なJSON形式ファイルに変換して tree2.json という名前で保存するPythonコードを書いてください。

藤原道長
├── 藤原妍子
├── 藤原威子
├── 藤原嬉子
│   └── 後冷泉天皇
├── 藤原彰子
├── 藤原教通
└── 藤原頼道
    ├── 藤原寛子
    └── 藤原師実

assistant:

もちろんです、ご指定のツリーを作成し、それをJSON形式で保存するPythonコードは以下の通りです:

import json
from treelib import Node, Tree

# Treeオブジェクトの生成
family_tree = Tree()

# ノードの追加
family_tree.create_node("藤原道長", "藤原道長")  # root node

family_tree.create_node("藤原妍子", "藤原妍子", parent="藤原道長")
family_tree.create_node("藤原威子", "藤原威子", parent="藤原道長")
family_tree.create_node("藤原嬉子", "藤原嬉子", parent="藤原道長")
family_tree.create_node("後冷泉天皇", "後冷泉天皇", parent="藤原嬉子")
family_tree.create_node("藤原彰子", "藤原彰子", parent="藤原道長")
family_tree.create_node("藤原教通", "藤原教通", parent="藤原道長")
family_tree.create_node("藤原頼道", "藤原頼道", parent="藤原道長")
family_tree.create_node("藤原寛子", "藤原寛子", parent="藤原頼道")
family_tree.create_node("藤原師実", "藤原師実", parent="藤原頼道")

# ツリーを辞書に変換
tree_dict = family_tree.to_dict(with_data=True)

# JSON形式に変換と保存
with open('tree2.json', 'w') as f:
    json.dump(tree_dict, f, ensure_ascii=False, indent=4)

このコードはJSON形式の家系図を' tree2.json 'というファイル名で保存します。


user:

次の家系図を階層的なJSON形式ファイルに変換して tree3.json という名前で保存するPythonコードを書いてください。

藤原安子
├── 円融天皇
│   └── 一条天皇
│       ├── 後一条天皇
│       └── 後朱雀天皇
│           └── 後冷泉天皇
└── 冷泉天皇
    └── 三条天皇

assistant:

以下のPythonコードで指定された家系図を作成し、それをJSON形式で "tree3.json" という名前のファイルとして保存します。

import json
from treelib import Node, Tree

# Treeオブジェクトの生成
family_tree = Tree()

# ノードの追加
family_tree.create_node("藤原安子", "藤原安子")  # root node

family_tree.create_node("円融天皇", "円融天皇", parent="藤原安子")
family_tree.create_node("一条天皇", "一条天皇", parent="円融天皇")
family_tree.create_node("後一条天皇", "後一条天皇", parent="一条天皇")
family_tree.create_node("後朱雀天皇", "後朱雀天皇", parent="一条天皇")
family_tree.create_node("後冷泉天皇", "後冷泉天皇", parent="後朱雀天皇")

family_tree.create_node("冷泉天皇", "冷泉天皇", parent="藤原安子")
family_tree.create_node("三条天皇", "三条天皇", parent="冷泉天皇")

# ツリーを辞書に変換
tree_dict = family_tree.to_dict(with_data=True)

# JSON形式に変換と保存
with open('tree3.json', 'w') as f:
    json.dump(tree_dict, f, ensure_ascii=False, indent=4)

このコードは tree3.json という名前のJSONファイルを出力します。


user:

ありがとうございます。それでは、家系図 tree.json と tree2.json を読み込んでマージした家系図を表示するPythonコードを書いてください。


assistant:

PythonのtreelibライブラリのTreeオブジェクトは、そのままマージすることはできません。なので、各ツリーごとにJSONデータをロードし、それぞれの辞書構造を生成します。その後、新しいツリーを生成し、それぞれの辞書データを元にノードを再帰的に追加していきます。以下にそのときのPythonコードを示します。

import json
from treelib import Node, Tree

def add_node(n, parent = None):
    name = list(n.keys())[0]
    merged_tree.create_node(name, name, parent = parent)
    children = n[name].get('children', [])
    for child in children:
        add_node(child, parent = name)

# JSON形式データの読み込み
with open('tree.json', 'r') as f:
    data1 = json.load(f)

with open('tree2.json', 'r') as f:
    data2 = json.load(f)

# Treeオブジェクトの生成
merged_tree = Tree()

# ツリーの作成
add_node(data1)
add_node(data2)

# 生成した家系図の表示
print(merged_tree.show(stdout=False))

このコードで、tree.jsontree2.jsonの内容をマージした家系図が表示されます。ただし、このコードでは2つのツリーに同じ名前のノードが存在するとエラーが発生しますので、その点はご注意ください。


user:

なるほど、確かにエラーになりましたね。tree.json の藤原道長と、tree2.json の藤原道長は同一人物なので、同じノードになるようにマージした家系図を作って表示するコードを書いてください。
上記のコードをベースに修正したコードを書いてください。

---------------------------------------------------------------------------
DuplicatedNodeIdError                     Traceback (most recent call last)
Cell In[10], line 23
     21 # ツリーの作成
     22 add_node(data1)
---> 23 add_node(data2)
     25 # 生成した家系図の表示
     26 print(merged_tree.show(stdout=False))

Cell In[10], line 6, in add_node(n, parent)
      4 def add_node(n, parent = None):
      5     name = list(n.keys())[0]
----> 6     merged_tree.create_node(name, name, parent = parent)
      7     children = n[name].get('children', [])
      8     for child in children:

File ~/.local/lib/python3.10/site-packages/treelib/tree.py:388, in Tree.create_node(self, tag, identifier, parent, data)
    383 """
    384 Create a child node for given @parent node. If ``identifier`` is absent,
    385 a UUID will be generated automatically.
    386 """
    387 node = self.node_class(tag=tag, identifier=identifier, data=data)
--> 388 self.add_node(node, parent)
    389 return node

File ~/.local/lib/python3.10/site-packages/treelib/tree.py:309, in Tree.add_node(self, node, parent)
    304     raise OSError(
    305         "First parameter must be object of {}".format(self.node_class)
    306     )
    308 if node.identifier in self._nodes:
--> 309     raise DuplicatedNodeIdError(
    310         "Can't create node " "with ID '%s'" % node.identifier
    311     )
    313 pid = parent.identifier if isinstance(parent, self.node_class) else parent
    315 if pid is None:

DuplicatedNodeIdError: Can't create node with ID '藤原道長'

assistant:

create_node()メソッドを呼び出す前に、出力ツリーに既に該当ノードが存在するかどうかをチェックすることで、指定されたエラーを克服できます。以下に修正版のコードを示します。

import json
from treelib import Node, Tree

def add_node(n, parent=None):
    name = list(n.keys())[0]
    if not merged_tree.contains(name):
        merged_tree.create_node(name, name, parent=parent)
    children = n[name].get('children', [])
    for child in children:
        add_node(child, parent=name)

# JSON形式データの読み込み
with open('tree.json', 'r') as f:
    data1 = json.load(f)

with open('tree2.json', 'r') as f:
    data2 = json.load(f)

# Treeオブジェクトの生成
merged_tree = Tree()

# ツリーの作成
add_node(data1)
add_node(data2)

# 生成した家系図の表示
print(merged_tree.show(stdout=False))

ここではadd_node()関数内で出力ツリーmerged_treeにノードがすでに存在しているかどうかを確認しています。ノードが存在しない場合のみcreate_node()を実行するため、同名のノード(この場合、"藤原道長")があってもエラーになりません。ですが、この方法は同名で別の人物の場合には適していません。


user:

おお、ありがとうございます。期待通りの結果になりました。

藤原師輔
├── 藤原兼家
│   ├── 藤原超子
│   ├── 藤原道兼
│   ├── 藤原道長
│   │   ├── 藤原妍子
│   │   ├── 藤原威子
│   │   ├── 藤原嬉子
│   │   │   └── 後冷泉天皇
│   │   ├── 藤原彰子
│   │   ├── 藤原教通
│   │   └── 藤原頼道
│   │       ├── 藤原寛子
│   │       └── 藤原師実
│   └── 藤原道隆
└── 藤原安子
    ├── 円融天皇
    └── 冷泉天皇

assistant:

素晴らしいですね!期待通りの結果が得られたようで、安心しました。

このコードにより、2つの家系図がマージされ、それぞれの家系図に存在する"藤原道長"というノードが同一のノードとして扱われるようになりました。

今後ともお気軽にご質問ください、お手伝いできることがあれば幸いです。


user:

ありがとうございます。それでは、家系図 merged_tree に対して、さらに tree3.json を読み込んでマージした家系図を表示するPythonコードを書いてください。


assistant:

merged_treeに対して、さらにtree3.jsonを読み込んでマージするためのPythonコードは以下のとおりです:


# JSON形式データの読み込み
with open('tree3.json', 'r') as f:
    data3 = json.load(f)

# ツリーの作成
add_node(data3)

# 生成した家系図の表示
print(merged_tree.show(stdout=False))

このコードでは、最初にtree3.jsonからJSONデータを読み込みます。次に、そのデータを元にノードを再帰的に追加しています。そして、最終的にマージされた家系図を出力しています。


user:

ありがとうございます。次のような結果が得られました。

藤原師輔
├── 藤原兼家
│   ├── 藤原超子
│   ├── 藤原道兼
│   ├── 藤原道長
│   │   ├── 藤原妍子
│   │   ├── 藤原威子
│   │   ├── 藤原嬉子
│   │   │   └── 後冷泉天皇
│   │   ├── 藤原彰子
│   │   ├── 藤原教通
│   │   └── 藤原頼道
│   │       ├── 藤原寛子
│   │       └── 藤原師実
│   └── 藤原道隆
└── 藤原安子
    ├── 円融天皇
    │   └── 一条天皇
    │       ├── 後一条天皇
    │       └── 後朱雀天皇
    └── 冷泉天皇
        └── 三条天皇

後朱雀天皇の御子として後冷泉天皇がいらっしゃるはずですが、後冷泉天皇は先に藤原嬉子の御子として記述されているので、2回目の記述は省略されているようですね。


assistant:

はい、その通りです。現在のPythonコードでは、家系図をマージする際に一度作成されたノードは再度作成されません。これは同一のノードが複数あるとエラーが発生するためです。

しかし、それが現実の家系図を正確に表現できない場合もあります。この場合のように、一人の人物(後冷泉天皇)が複数の視点(藤原嬉子の子、後朱雀天皇の子)から参照されている場合には、現在のコードではこの恒等性を表現できません。

これを解決するには、ツリーのノードにユニークな識別子を付け、そのノードが何度も参照されることを許容する別のデータ構造を使用する必要があります。しかし、このタイプのデータ構造は、現在使用しているtreelibライブラリではサポートされていません。

また、このような家系図の表現は、それが直感的に理解しやすい階層的ツリー構造から離れ、グラフ構造となります。グラフ構造を扱うためのライブラリやデータベースも存在しますが、それは別のトピックとなります。


user:

なるほど。ありがとうございました。また色々と教えてくださいね。


assistant:

どういたしまして!お手伝いできてうれしいです。何か他に質問がございましたら、いつでもお気軽にお問い合わせください。お待ちしております!


1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2