PythonAlgorithm

Pythonでグラフネットワークを可視化する方法

公開日:2023-01-09 更新日:2023-06-12

今回は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で追加

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をまとめて追加

# 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で一気に追加

# 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問の入力例を参考につくってみました。

Twitter Share