アオカケスの鳥かご

日々の出来事を綴っていきたい

NetworkXを用いた関係図の作成

最近、とある学会に提出する原稿をまとめている際に関係図を作成することがありました。

原稿に載せるのでデジタル化したかったのですが、どんな形になるか分からなかったので手書きで作成したところ、とてもカオスな感じになりました。

f:id:aokakes:20190516182845j:plain
とてもカオスな関係図

これでも全体の半分くらいのデータで、人間の手で全部反映させるのは現実的ではありません。さすがに自動化しようということで見つけたのがpythonのライブラリであるNetworkXでした。

NetworkXについて

NetworkXはグラフやネットワーク理論系の計算を行うためのライブラリで、関係図などの作成が容易に出来ます。


また、NetworkXはバージョンによってAPIの叩き方が異なる部分が多くあります。

google検索で上位に出てくるページは1.X系が多く、コピペしても動かないことがありますので注意が必要です。
※普通にNetworkXをインストールすると2.X系になります。

なお、ここで紹介するプログラムは2.x系を想定しています。

NetworkXのインストール

pipでインストールするだけです。

$ sudo pip3 install networkx

読み込むCSVデータ

001,025
001,038
001,069
001,037,038
001,067,069
⋮
070,089
080,082
083,084
088,089
089,092

実装

ファイルの読み込み

node_list = []
with open('sample.csv','r') as f:
    f_lines = f.readlines()
    
    for i in range(len(f_lines)):
    
    # 読み込んだCSVがBOMありのときは \ufeff を削除する
    if i == 0:
        f_lines[i] = f_lines[i].replace('\ufeff','')
    
    # 改行文字を削除
    f_lines[i] = f_lines[i].replace('\n','').split(',')
    	
    # リストに追加
    node_list.append(f_lines[i])

普通に読み込んで普通にリストに追加しているだけです。
csvの作り方によってはBOMありになっていたりするので、1つ目のデータに \ufeff があったら削除してあげます。

グラフの作成

# ノードの出現回数をカウント
node_count = collections.Counter(itertools.chain.from_iterable(node_list))
 
# グラフを作成
G = nx.Graph()
 
# ノードに属性を追加するときは辞書で入れる
G.add_nodes_from([(node, {'count':node_count[node]}) for node in node_count])
 
for nodes in node_list:
    #print(nodes)
    for node0,node1 in itertools.combinations(nodes, 2):
        # 万が一、ノードが存在しないときは何もしない
        if not G.has_node(node0) or not G.has_node(node1):
            continue
 
        # 既にエッジが存在するときは何もしない
        if G.has_edge(node0, node1):
            G.edges[node0, node1]['weight'] += 1
 
        # エッジが存在しないときは新たに追加
        else:
            G.add_edge(node0, node1)
            G.edges[node0, node1]['weight'] = 1

ノードの大きさを出現回数に応じて変化させるためにあらかじめ数えておきます。

ノードに属性を加える部分が1.x系と異なります。
1.x系では add_edge() で属性を追加できるようですが、同じように書くと2.x系ではエラーになります。

グラフの描画

plt.figure(figsize=(20,20))
 
# レイアウトの調整
pos = nx.spring_layout(G, k=0.4)
 
# ノードの大きさを出現回数に応じて大きくする
node_size = [ d["count"]*300 for (n,d) in G.nodes(data=True)]
nx.draw_networkx_nodes(G, pos, node_color="#0000FF", alpha=0.5, node_size=node_size)
 
# エッジのweightに応じて太さを変える
edge_width = [ d["weight"]*0.9 for (u,v,d) in G.edges(data=True)]
nx.draw_networkx_edges(G, pos, edge_color="#000000", alpha=0.7, width=edge_width)
 
# フォントの設定
nx.draw_networkx_labels(G, pos, font_color="#000000")
 
plt.axis('off')
plt.savefig("sample.png")
plt.show()

figure() で画像サイズを指定します。
それぞれ ×100 した値のピクセル数になります。この場合だと2000×2000です。


nx.spring_layout() でレイアウトの調整が出来ます。
k の値を大きくすると中心から離れたところに描画します。ノードの数が多いときはこの値を大きめに設定することでノードの位置が被りにくくなります。


draw_networkx_nodes() でノードを描画します。
オプションの node_color は16進数のカラーコードか色名(whiteやblackなど)で設定できます。ただし、3桁の短縮コードは設定できません。

alpha は透過度を示していて、1.0 から 0 に近づくほど透明になっていきます。
ノード数が多いとノードの位置が重なって見ずらくなってしまうので、少し透明にしてあげると良さげです。


draw_networkx_edges() でエッジを描画します。
オプションはノードと特に変わりません。


draw_networkx_labels() でフォントの設定ができます。
font_family = "hoge" で好きなフォントを設定できます。font_weight = "bold" にすることで太字にすることも可能です。

ソースコード

オプションに設定する値は少しずつ変えて、良さげな図が出来上がるまで調整するしかありません。
同じ値でもプログラムを実行して図を吐きだす度に微妙に配置が変わるので、お気に入りの図が出来上がるまで頑張りましょう。

成果物

f:id:aokakes:20190516192851p:plain
見やすくなった(?)関係図
相変わらずゴチャゴチャしてはいますが、手書きの関係図よりは見やすくなりました。

手書きだとどうしても見落としがあったり、時間も掛かるものです。
しかし、プログラムで書いたら一瞬な上に見落としもありません(プログラムにミスが無い限り)。そしてカッコいい。


今後も使えそうなプログラムが出来たのではないかなと思います。