おはこんばんちわ。今日も引き続き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()
このコードではまず、Tree
とNode
を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引数
tag
はノードの表示名(ユーザが見る名前)を指定します。これはツリーを表示した際にノードとして表示させる名前です。 - 第2引数
identifier
はそのノードを一意に特定するためのIDを指定します。これは内部的にノードを特定するために使用され、通常はユーザには表示されません。
なので、この2つの引数の値が同じであることは特に問題ありません。
これらが同じ場合と異なる場合の例を挙げます。
-
同じ場合:
- ファミリーツリー、組織図など、ノードの名前が一意である(すなわち、同じ名前を持つノードが2つ存在しない)場合、
tag
とidentifier
は同じにしても問題ありません。先程の家系図コード例が該当します。
- ファミリーツリー、組織図など、ノードの名前が一意である(すなわち、同じ名前を持つノードが2つ存在しない)場合、
-
異なる場合:
- しかし、ノードの名前が一意でない場合(例えば、クラス全体の生徒さんをノードとした木を作る場合など)、一つの名前に対して複数のIDが必要となるかもしれません。そのような場合、
tag
とidentifier
は異なる値にします。例えば、tag
には生徒の名前を設定し、identifier
には一意の生徒IDを設定することができます。
- しかし、ノードの名前が一意でない場合(例えば、クラス全体の生徒さんをノードとした木を作る場合など)、一つの名前に対して複数の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:
はい、おっしゃる通りです。treelib
のto_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.json
とtree2.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:
どういたしまして!お手伝いできてうれしいです。何か他に質問がございましたら、いつでもお気軽にお問い合わせください。お待ちしております!