今回はPythonでグラフネットワークを可視化する方法について簡単にまとめていきます。
networkx
というライブラリを使います。
まず、この記事でのグラフの定義について明らかにしておきます。
通常グラフというと、折れ線グラフや円グラフなどを思い浮かべる人が多いかと思います。
ただし、アルゴリズムの文脈でのグラフはモノとモノを結ぶネットワークを指します。頂点がいくつかあって、辺が頂点と頂点を結んでいるケースです。
例えば、SNSで誰と誰がフレンドであるなど結びつきを表す情報などを想像すると分かりやすいと思います。
電車の路線図なんかもそうですね。
グラフにはいろいろと種類があるのですが、この記事では、無向グラフ
、有向グラフ
、重み付きグラフ
の3種類のグラフの可視化を行っていきます。
networkx
と描画もするのでmatplotlib
も入れておきます。
pip install networkx
pip install matplotlib
それでは早速グラフの描画をしていきます。
無向グラフはいちばん基本的なグラフでしょうか。
その名の通り、頂点と頂点を結ぶ辺に向きがありません。
上述した電車の路線図が良い例でしょうか。例えば品川駅と東京は双方向に行き来ができます。これが無向ということです。
描画をしてみましょう。
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.Graph()
graph.add_node('tokyo')
graph.add_node('shinagawa')
graph.add_edge('tokyo', 'shinagawa')
nx.draw(graph, with_labels=True)
plt.show()
このように描画がされます。
基本的にはnode(=頂点)を追加して、edge(=辺)を加えるという流れです。
nodeはiterableを指定して一気に加えることもできます。
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.Graph()
graph.add_node('tokyo')
graph.add_node('shinagawa')
graph.add_edge('tokyo', 'shinagawa')
# iterableで追加
an_iterable = ['kanda', 'ueno']
graph.add_nodes_from(an_iterable)
# edgeを追加
graph.add_edge('tokyo', 'kanda')
graph.add_edge('kanda', 'ueno')
nx.draw(graph, with_labels=True)
plt.show()
# edgeをiterableで追加
# tupleのリストを指定する。
# 大崎、五反田、目黒はノードを追加していないがよしなに追加してくれる。
stations = ['shinagawa', 'osaki', 'gotanda', 'meguro']
edges = []
for i in range(len(stations) - 1):
edge = (stations[i], stations[i + 1])
edges.append(edge)
graph.add_edges_from(edges)
# add_pathで一気に追加できる。
# 最後に上野をつないだ。
nx.add_path(graph, ['meguro', 'ebisu', '...', 'ueno'])
つづいて有向グラフですが、こちらは頂点と頂点をつなぐ辺に向きがあるネットワークです。
SNSのtwitterをイメージすると分かりやすいです。
例えば私のアカウントは@kuri_tter
で、シンガーソングライターの柴田聡子さん@sbttttt
をフォローしています。
しかし、柴田聡子さんは私のことをフォローしていません。
こういう状態を表現するのに有向グラフを使います。
これを作るのは簡単で、インスタンス化する際にnx.DiGraph()
とするだけです。
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.DiGraph()
me = '@kuri_tter'
sbtstk = '@sbttttt'
graph.add_node(me)
graph.add_node(sbtstk)
graph.add_edge(me, sbtstk)
nx.draw(graph, with_labels=True)
plt.show()
例えば@fdrbdr
さんとは相互フォローになっています。
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.DiGraph()
me = '@kuri_tter'
sbtstk = '@sbttttt'
graph.add_node(me)
graph.add_node(sbtstk)
graph.add_edge(me, sbtstk)
# 相互フォローを追加
it_pigeon = '@fdrbdr'
graph.add_node(it_pigeon)
graph.add_edges_from([(me, it_pigeon), (it_pigeon, me)])
nx.draw(graph, with_labels=True)
plt.show()
このようなフォロー関係を表すグラフができました。
続いて重み付きグラフです。
こちらは辺に重みを付けるときに使います。
重みとは駅の例でいいますと、移動にかかる時間のことを指します。
重み付きグラフの場合は少し手数が増えて、ノードごとのポジションを指定しないといけません。
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.Graph()
# 追加する駅
stations = ['tokyo', 'shinagawa']
# nodeを追加する
graph.add_nodes_from(stations)
# positionをx,yで指定
pos = {'tokyo': (3, 3),
'shinagawa': (5, 5)
}
# (始点,終点,重み)でエッジを指定
graph.add_weighted_edges_from([('tokyo', 'shinagawa', 5)])
# エッジのラベルを生成
edge_labels = {(i, j): w for i, j, w in graph.edges(data=True)}
# グラフを描画
nx.draw(graph, pos=pos, with_labels=True)
# エッジラベルを描画
nx.draw_networkx_edge_labels(graph, pos=pos, edge_labels=edge_labels)
plt.show()
以上のように重みをつけて描画がされました。
{'weight': 10}
となっているのが気になる場合は以下のようにしましょう。
内部的な辞書データを文字列として表示しているので、keyを指定すれば数字だけ取り出せます。
# keyを指定する
edge_labels = {(i, j): w['weight'] for i, j, w in graph.edges(data=True)}
数字だけ取り出せました。
以上、networkxを活用してグラフネットワークの描画方法について簡単にまとめました。
ここからは余談で、AtCoderなどの競技プログラミングで役に立つ場面があると思いました。
具体的にはグラフ系の問題で、標準入力を受け取ったあとの可視化に使えると考えています。
標準入力だけみても、それがどんな図なのかイメージすることは難しいです。
行き詰ったときに可視化してみると何か閃きがあるかもしれません。
通常、標準入力で受け取った情報は、連結リストか辞書型のデータ構造で持つかと思います。
以下のようなデータを渡すとプロットする関数を書いてみました。
import networkx as nx
import matplotlib.pyplot as plt
def plot_graph_by_dict(data: dict, n: int, start_one: bool = True, directed: bool = False):
"""
辞書を元に無向グラフを可視化する
"""
if directed:
graph = nx.DiGraph()
else:
graph = nx.Graph()
for i in range(n):
if start_one:
graph.add_node(i + 1)
else:
graph.add_node(i)
for node, edges in data.items():
for edge in edges:
graph.add_edge(node, edge)
fig, ax = plt.subplots()
nx.draw(graph, with_labels=True)
fig.tight_layout()
plt.show()
def plot_graph_by_connected_list(data: list, n: int, start_one: bool = True, directed: bool = False):
"""
連結リストを元に無向グラフを可視化する
"""
if directed:
graph = nx.DiGraph()
else:
graph = nx.Graph()
for i in range(n):
if start_one:
graph.add_node(i + 1)
else:
graph.add_node(i)
for node in range(len(data)):
for edge in data[node]:
graph.add_edge(node, edge)
fig, ax = plt.subplots()
nx.draw(graph, with_labels=True)
fig.tight_layout()
plt.show()
たとえば以下のようなデータを渡した場合。
def job():
n = 8
# 辞書型の場合
d = {1: [3, 5, 2, 4],
2: [6, 4, 1, 7, 5, 3, 8],
3: [1, 8, 6, 4, 5, 2, 7],
4: [7, 6, 3, 2, 1, 5],
5: [6, 1, 3, 2, 4, 7],
7: [4, 2, 3, 6, 5],
6: [2, 5, 3, 4, 7],
8: [3, 2]}
plot_graph_by_dict(d, n, directed=False)
n = 5
# 連結リストの場合
d = [[], [2], [1], [1], [5], [4]]
plot_graph_by_connected_list(d, n, directed=True)
if __name__ == '__main__':
job()
データはAtCoder Beginner Contest 284の以下2問の入力例を参考につくってみました。