プログラミング原人の進化ログ

プログラミング原人の進化論

オレ プログラミング ベンキョウ スル。マナンダ コト カク。

Python上でGraphvizを使って綺麗なグラフを描く

グラフを描きたくなることってありますよね。ここでいうグラフはmatplotlibで描くような類のものではなくて、ノードとエッジからなる、グラフ理論でいうところのグラフです。
で、そのグラフを綺麗に描くことのできるツールがGraphvizです。DOTなる言語で記述されたファイルを読み込んでグラフを描画します。でも僕はDOT言語は知らないし、できれば自分の使える言語で描きたいわけです。Pythonとかね。そこで登場するのが、Graphvizpython上で使うためのラッパーとして提供されているライブラリgraphvizです。
グラフをかけるpythonのライブラリとしてNetworkXが有名ですが、これは分析がメインなので、グラフを描画するためだけならgraphvizを使うのが良いと思います。描画に関してはNetworkXよりも色々なことができます。

目標

Pythonでグラフをかっこよくかけるようになる。写経を通して自然にgraphvizの基本的な使い方を覚える。

インストール

Graphviz本体のインストール
$ brew install graphviz
ラッパーのインストール
$ pip install graphviz

基本的な使い方

公式がすごくわかりやすいです。

graphviz.readthedocs.io

User GuideをやってからExampleをやってみれば基本的なことはわかると思います。でも詳しい情報はばらけているのでまとめておくというのがこの記事の目的です。あと、英語読むのめんどくさいよ、っていう人向けです。

基本的な流れ

基本的には、

  1. グラフオブジェクトの生成
  2. ノードやエッジを追加
  3. (必要であれば)保存、描画

という流れでグラフを描いていきます。

実際にコードを書いていきましょう。なお、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

f:id:programgenjin:20190222214644p:plain

dg

f:id:programgenjin:20190222214656p:plain

必要であればグラフを保存します。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()

f:id:programgenjin:20190222214644p:plain

画像はデフォルトでは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()

f:id:programgenjin:20190222220019p:plain

ラベルをつける

ラベルをつけることができます。特にノードに長い名前をつけるときや、エッジに文字を付加するときに便利です。

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()

f:id:programgenjin:20190222220452p:plain

ノードの名前を表示したくないとき

ラベル名を空文字にすれば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()

f:id:programgenjin:20190222221629p:plain

色を付ける

ノードやエッジの色は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()

f:id:programgenjin:20190222221658p:plain

グラフの一部をグループ化する

いくつかのノードがまとめて構造化することもできます。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()

f:id:programgenjin:20190222225327p:plain

その他

他にも色々な属性があります。紹介した機能だけでも結構いろいろできると思いますが、足りなければ次のページで調べてみると良いでしょう。

www.graphviz.org

まとめ

graphvizを使ってかっこよくグラフを描けるようになりました。
簡単ですね!
pythonで使えるというのはありがたいです。jupyterとすごく相性が良いので、ぜひ使っていただきたい。