Python Matplotlib で Chord Diagram

#Chord Diagram
Chord diagram はグループ間同士の結びつきや、関連性の大きさを図示するときに使う手法。生物学の分野では、よく環状ゲノムを有する生物(バクテリア等）に対して、ゲノム内の情報間の関連性を図示するときに利用される(専用のパッケージではCircosが有名)。Rにも専用のlibraryがある(https://qiita.com/uchim/items/f9eea0ef5caa10067b02) 。しかし、pythonでと思って探してみたが見つからない。ということで、無理やりmatplotlibでそれっぽいものを作成してみた。

#利用データ
https://qiita.com/uchim/items/f9eea0ef5caa10067b02 でも使用されている山手線駅間の所要時間のデータを利用した。

#スクリプト その1 (極座標系とlocus plot)
Chord diagram をするためには、まずpolar plot(極座標系)を扱う必要がある。

```py
import os
import sys
import collections
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot  as plt
import matplotlib.path    as mpath
import matplotlib.patches as mpatches

class CD(object):
    def __init__(self,figsize=(10,10)):
        self.figure = plt.figure(figsize=figsize)
        self.ax     = plt.subplot(111, polar=True)
        self.ax.set_theta_zero_location("N") #北(12時)を起点にする
        self.ax.set_theta_direction(-1) #時計回り
        self.ax.set_ylim(0,1000)
        self.ax.spines['polar'].set_visible(False) #軸を隠す
        #とりあえず軸関連は全部隠す
        self.ax.xaxis.set_ticks([])
        self.ax.xaxis.set_ticklabels([])
        self.ax.yaxis.set_ticks([])
        self.ax.yaxis.set_ticklabels([])
        #グループに関する情報を保存するためのdict
        self.locus_dict = collections.OrderedDict()
````
これで、極座標系のテンプレートは完成。次に、各グループごとのlocusをプロットしてみる。これは、barplotをつかうと比較的簡単にできる。

```py
    #matrixは利用するデータ（２次元配列）
    def locus_plot(self, matrix, interspace=0.002, features=True, bottom=900, height=50, color_list=[], edge_color_list=[]):
        #locusの色の設定
        if len(color_list) == 0:
            for i in range(len(matrix)):
                color_list.append("#E3E3E3")
        if len(edge_color_list) == 0:
             for i in range(len(matrix)):
                 edge_color_list.append("#000000")

        self.sum_length = 0
        for i, row in enumerate(matrix):
            self.locus_dict[i] = {}
            self.locus_dict[i]["length"] = sum(row) #各行の合計
            self.locus_dict[i]["data"]   = row
            self.sum_length += self.locus_dict[i]["length"]
        #sum_lengthはmatrix全体の合計値
        self.interspace = interspace * self.sum_length #interspaceはlocus間の距離
        self.sum_length += self.interspace * len(self.locus_dict.keys()) #sum_lengthの補正

        start = self.interspace
        end   = 0
        for i, Id in enumerate(self.locus_dict.keys()):
            self.locus_dict[Id]["start"] = int(start) #locusの開始位置
            end   = start + self.locus_dict[Id]["length"]
            self.locus_dict[Id]["end"] = int(end) #locusの終了位置
            pos   = (start+end) * 0.5 * 2 * np.pi / self.sum_length #極座標系におけるlocusの中心座標
            
            #中心座標を起点に、長さが 2.0*np.pi*(end-start)/self.sum_length の barplot。 bottom (0~1000) と height は適当に。
            self.locus_dict[Id]["bar"] = self.ax.bar([pos,pos], [0,height], bottom=bottom, width=2.0*np.pi*(end-start)/self.sum_length, color=color_list[i], linewidth=1, edgecolor=edge_color_list[i]
            start = end + self.interspace
```
ここまでできれば、以下のように各グループのlocusをプロットできる。

#実行結果 その1 (locusのプロット)
```py
if __name__ == "__main__":
    data    = [[0,7,12,14,18,22,25,28,24,21,17,14,10,7],
              [7,0,5,7,11,15,18,23,31,28,24,21,17,14],
              [12,5,0,2,6,10,13,18,22,25,29,26,22,19],
              [14,7,2,0,4,8,11,16,20,23,27,28,24,21],
              [18,11,6,4,0,4,7,12,16,19,23,26,28,25],
              [22,15,10,8,4,0,3,8,12,15,19,22,26,29],
              [25,18,13,11,7,3,0,5,9,12,16,19,23,26],
              [28,23,18,16,12,8,5,0,4,7,11,14,18,21],
              [24,31,22,20,16,12,9,4,0,3,7,10,14,17],
              [21,28,25,23,19,15,12,7,3,0,4,7,11,14],
              [17,24,29,27,23,19,16,11,7,4,0,3,7,10],
              [14,21,26,28,26,22,19,14,10,7,3,0,4,7],
              [10,17,22,24,28,26,23,18,14,11,7,4,0,3],
              [7,14,19,21,25,29,26,21,17,14,10,7,3,0]]
    colors = sns.color_palette("Set3", 12)
    colors = list(map(matplotlib.colors.rgb2hex,colors))
    colors = colors + colors
    cd = CD()
    cd.locus_plot(data,interspace=0.005,color_list=colors)
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167957/de8e0b37-e8d0-f073-c31a-123fbe3276c2.png)

#スクリプト その2 (Chord diagram)
Chord Diagramのカーブがどうなってるか詳細は知らないが、多分ベジェ曲線だろうということで話を進めます。あまりしられてなさそうだが、実は matplotlib ではベジェ曲線が描けるメソッドがちゃんと用意されている。(https://matplotlib.org/users/path_tutorial.html) 。なので、これを利用することで、それっぽいものを実装することができた (このメソッドを見つけるまではあきらめそうだった、、)。

```py
    #ややこしいが、topの値はlocus_plotのbottomと同じにするといい
    def chord_plot(self, top=900, bottom=20, color_list=[]):
        for Id in sorted(self.locus_dict.keys()):
            locus_info = self.locus_dict[Id]
            send = locus_info["start"]
            for i, num in enumerate(self.locus_dict[Id]["data"]):
                sstart = send
                send   = send + num
                #chordの始点側の両端の座標
                sstart_pos = 2 * sstart * np.pi / self.sum_length
                send_pos   = 2 * send * np.pi / self.sum_length
                #chordの終点側の両端の座標
                ostart = self.locus_dict[i]["start"] + sum(self.locus_dict[i]["data"][:Id])
                oend   = ostart + self.locus_dict[i]["data"][Id]
                ostart_pos = 2 * ostart * np.pi / self.sum_length
                oend_pos   = 2 * oend * np.pi / self.sum_length
                if sstart_pos == ostart_pos:
                    pass
                else:
                    #chordのPathのプロット。しかし、改善の余地がある、、、LINETOで結んでるところはchordが大きいと直線であることがバレる。。。
                    Path      = mpath.Path
                    path_data = [(Path.MOVETO,  (sstart_pos, top)),
                                 (Path.CURVE3,  (sstart_pos, bottom)),
                                 (Path.CURVE3,  (oend_pos,   top)),
                                 (Path.LINETO,  (ostart_pos, top)),
                                 (Path.CURVE3,  (ostart_pos, bottom)),
                                 (Path.CURVE3,  (send_pos,   top)),
                                ]
                    codes, verts = list(zip(*path_data))
                    path  = mpath.Path(verts, codes)
                    patch = mpatches.PathPatch(path, facecolor=color_list[Id], alpha=1.0)
                    self.ax.add_patch(patch)
```

#実行結果 その2 (Chord Diagram)
```py
    cd = CD()
    cd.locus_plot(data,interspace=0.005,color_list=colors)
    cd.chord_plot(color_list=colors)
    plt.savefig("test.pdf")
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/167957/b59dc8d0-1e36-a8db-ef4c-d6cb36cfd25f.png)

少し、 https://qiita.com/uchim/items/f9eea0ef5caa10067b02 の結果と違う気もするが、それっぽいものが実装できた。

#スクリプト その3 (テキスト情報の追加)
加筆予定。

#学んだこと
めちゃくちゃ面倒だった。。。思いつきで実装してはいけない。しかし、Matplotlibでもきれいな極座標系のプロットができることを学べたのは大きな収穫でした。