Python上でGraphvizを使って綺麗なグラフを描く
グラフを描きたくなることってありますよね。ここでいうグラフはmatplotlibで描くような類のものではなくて、ノードとエッジからなる、グラフ理論でいうところのグラフです。
で、そのグラフを綺麗に描くことのできるツールがGraphvizです。DOTなる言語で記述されたファイルを読み込んでグラフを描画します。でも僕はDOT言語は知らないし、できれば自分の使える言語で描きたいわけです。Pythonとかね。そこで登場するのが、Graphvizをpython上で使うためのラッパーとして提供されているライブラリgraphvizです。
グラフをかけるpythonのライブラリとしてNetworkXが有名ですが、これは分析がメインなので、グラフを描画するためだけならgraphvizを使うのが良いと思います。描画に関してはNetworkXよりも色々なことができます。
基本的な使い方
公式がすごくわかりやすいです。
User GuideをやってからExampleをやってみれば基本的なことはわかると思います。でも詳しい情報はばらけているのでまとめておくというのがこの記事の目的です。あと、英語読むのめんどくさいよ、っていう人向けです。
基本的な流れ
基本的には、
- グラフオブジェクトの生成
- ノードやエッジを追加
- (必要であれば)保存、描画
という流れでグラフを描いていきます。
実際にコードを書いていきましょう。なお、Jupyter notebookを使うとノート上でグラフを確認できるので非常に便利です。
まず、オブジェクトを生成します。無向グラフを描くGraphモジュールと有向グラフを描くDigraphモジュールをインポートしておきます。
from graphviz import Graph from graphviz import Digraph g = Graph(format='png') dg = Digraph(format='png')
次に、ノードとエッジを追加していきます。
# 無向グラフ # nodeを追加 g.node('1') g.node('2') g.node('3') # edgeを追加 g.edge('1', '2') g.edge('2', '3') g.edge('3', '1') # 有向グラフ dg.node('1') dg.node('2') dg.node('3') dg.edge('1', '2') # 1 -> 2 dg.edge('2', '3') # 2 -> 3 dg.edge('3', '1') # 3 -> 1
jupyter notebook上でグラフを確認するためには、次のようにします。
g
dg
必要であればグラフを保存します。renderメソッドの引数viewをTrueにすると、保存するとともに画像が表示されます。なお、viewメソッドを使うことで画像を表示することができます。
g.render('./graph', view=True) dg.render('./dgraph', view=True)
なお、エッジを指定するだけで足りないノードが勝手に追加されます。こんな風に。
g = Graph() g.edge('1', '2') g.edge('2', '3') g.edge('3', '1') g.view()
画像はデフォルトではpdfで保存されますが、色々な形式で保存することができます。
オブジェクト生成の段階で指定するか、
g = Graph(format='png')
それ以外のタイミングで指定することもできます。
g.format = 'svg'
見た目を整える
もう少し詳しい使い方を説明します。
属性を指定することで、グラフの見た目をいじることができます。属性は、グラフ全体、ノード、エッジに対して説明することができます。
attrメソッドでまとめて指定するか、あるいはノードやエッジに個別に指定することもできます。
例を見てみましょう。
ノードの形状
次はノードの形状を指定する例です。
from graphviz import Graph g = Graph(format='png') g.attr('node', shape='circle') g.node('0') g.node('1') g.attr('node', shape='square') g.node('2') g.attr('node', shape='star') g.node('3') g.node('4', shape='circle') g.view()
ラベルをつける
ラベルをつけることができます。特にノードに長い名前をつけるときや、エッジに文字を付加するときに便利です。
g = Digraph(format='png') for i in range(3): g.node(str(i), label='state{0}'.format(i+1)) edges = [[0, 0], [0, 1], [1, 0], [1, 2], [2, 0]] edge_labels = ['0.5', '0.5', '0.3', '0.7', '1.0'] for i, e in enumerate(edges): g.edge(str(e[0]), str(e[1]), label=edge_labels[i]) g.view()
ノードの名前を表示したくないとき
ラベル名を空文字にすればOKです。
g = Graph(format='png') g.attr('node', shape='circle', label='') edges = [[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [2, 6]] for i, j in edges: g.edge(str(i), str(j)) g.view()
色を付ける
ノードやエッジの色はcolor、文字色はfontcolor、塗りつぶすときはfillcolorという属性で指定します。塗りつぶす場合はstyle='filled'とします。
色の一覧:
Color Names
g = Graph() g.attr('node', shape='circle', color='darkgreen') g.attr('edge', color='darkgreen') g.node('0', style='filled', fillcolor='gray85', fontcolor='red') g.edge('0', '1') g.edge('0', '2') g.view()
グラフの一部をグループ化する
いくつかのノードがまとめて構造化することもできます。subgraphメソッドを使います。
nameをclusterから始まる物にすることによってひとかたまりの領域であると認識され、自動的に枠線がつきます。
g = Graph() g.attr('node', shape='circle') with g.subgraph(name='cluster_root') as c: c.attr(color='white', label='root') # デフォルトではgraphに適用される c.node('0') c.edges([('0', '1'), ('0', '2')]) with g.subgraph(name='cluster_0') as c: c.attr(color='blue') c.edges([('1', '3'), ('1', '4')]) with c.subgraph(name='cluster_00') as cc0: cc0.attr(color='darkgreen') cc0.node('3') with c.subgraph(name='cluster_01') as cc1: cc1.attr('graph', color='darkgreen') cc1.node('4') with g.subgraph(name='cluster_1') as c: c.attr(color='blue') c.edges([('2', '5'), ('2', '6')]) with c.subgraph(name='cluster_10') as cc0: cc0.attr(color='darkgreen') cc0.node('5') with c.subgraph(name='cluster_11') as cc1: cc1.attr(color='darkgreen') cc1.edges([('6', '7'), ('6', '8')]) with cc1.subgraph(name='cluster_110') as ccc0: ccc0.attr(color='red') ccc0.node('7') with cc1.subgraph(name='cluster_111') as ccc1: ccc1.attr(color='red') ccc1.node('8') g.view()