円形データ可視化パッケージ「pyCirclize」を開発してみた話

https://github.com/moshi4/pyCirclize

# はじめに

データ間のネットワークやフローのような相互関係を表現する際には円形でのデータ可視化がよく行われます。下記のような図は**Chord Diagram**と呼ばれており、各データの相互関係を分かりやすく美しく可視化することができます。
![chorddiagram_example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/8b411224-5daf-1b01-ff7b-aaaa749c4e07.png)
**↑ Chord Diagramプロット例** (https://jokergoo.github.io/circlize_book/book/the-chorddiagram-function.html)

またバイオインフォマティクス(情報生物学)分野ではゲノム情報や遺伝子ネットワークの可視化表現に**Circos**という円形データ可視化ツール(Perlモジュール)が広く使われており、様々なデータの数値・統計量・相互関係の円形可視化がされているのをよく目にします。
![circos-sample-panel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/41c4e1bf-a007-c51e-b776-f1a7fbd8b327.png)
**↑ Circosプロット例** (http://circos.ca/)

本記事では、Chord DiagramやCircosのような円形データ可視化をPythonのAPIから簡単かつ柔軟に実行できるように個人開発したパッケージ「**pyCirclize**」([GitHub](https://github.com/moshi4/pyCirclize) / [Document](https://moshi4.github.io/pyCirclize))について紹介します。例として、以下のような円形データ可視化を実行することができます。

![pyCirclize_gallery.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/c62cc007-5053-2f12-c181-7513e1de899d.png)

# 開発目的

Rでは[circlize](https://github.com/jokergoo/circlize)が円形データ可視化パッケージとして有名で多くのユーザーに利用されています。circlizeは円形データ可視化のための高水準・低水準APIが数多く用意されており、ユーザーは柔軟に円形データ可視化のためのコードを記述することができます。
その一方、Pythonではcirclizeほど機能が豊富かつユーザーから広く利用されている円形データ可視化パッケージはまだ見当たらないのが現状です。そこで、Pythonでもcirclizeのような可視化ができるようにすることを目的として「pyCirclize」の開発に取り組みました。

# 実装機能

pyCirclizeは名前からも分かる通り、circlizeを参考にして開発をしており、円形描画レイアウトの考え方や各種描画API仕様等で似通っている部分があります([参考：circlize - Circular layout](https://jokergoo.github.io/circlize_book/book/circular-layout.html))。Pythonの標準的な描画パッケージであるmatplotlibをベースにcirclizeっぽいデータ可視化機能を実装しています。

本項では、pyCirclizeで可視化をする上での基本的なレイアウト設計とデータ描画機能について簡単に説明します。

- **円形描画レイアウト設計**
円形描画レイアウトはユーザーの入力する **1. データ数・サイズ**、**2. 度数範囲指定(-360°〜360°)**、**3. スペース度数指定**、によって決定されます。入力データ一つに対して一つのセクター(Sector)を定義し、データサイズ・度数範囲・スペース度数に応じて円形領域における適切なセクターサイズが決定されます。各セクター内には各種データを描画するためのトラック(Track)を自由な位置に複数配置することができ、トラックに定義された各描画APIを呼び出すことでトラック内にデータを描画できます。
例えば、下図は5つのデータ(`A=10、B=15、C=12、D=20、E=15`)で`度数範囲=0〜360°`、`スペース度数=5°`に対するセクター及びトラックの描画レイアウト設定例です。
![sector_track.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/5fd61e62-dbef-4d23-fa6c-47c47a803b0b.png)

- **データ描画機能**
セクターに設定したデータに紐づく何かしらの数値・統計量をLine・Bar・Point等のグラフあるいはArrow・Rectangle・Tree等の図形としてトラックに描画することができます。ユーザーが入力したXY直交座標系データをトラック範囲内の適切な極座標系データへ変換を行った上で描画が実行されます。またデータ内あるいはデータ間でのネットワークやフロー等の関連性を示すためのデータリンク(無方向・順方向・逆方向・双方向の指定可)を描画する機能も実装されています。
文章のみで説明されても少しイメージしづらいと思います。下図のデータ可視化例も参照すると、どのようにデータ描画ができるのか直感的に分かりやすいかなと思います。
![track_plot_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/051fc7fa-d684-c3b3-283c-c278d22eae87.png)

# インストール方法

pyCirclizeは[PyPI](https://pypi.org/project/pycirclize/)と[conda-forge](https://anaconda.org/conda-forge/pycirclize)にそれぞれパッケージを登録しているので、`pip`または`conda`コマンドでインストール可能です。

**PyPIパッケージ**
```
pip install pycirclize
```

**Condaパッケージ**
```
conda install -c conda-forge pycirclize
```

# API実行例

pyCirclizeの基礎・実践のAPI実行例について、いくつか参考に記載します。実践例にはバイオインフォマティクス(情報生物学)分野のゲノム可視化例が多いです。

## 基礎

pyCirclizeの[Getting Started](https://moshi4.github.io/pyCirclize/getting_started/)ドキュメントより実行例を一部抜粋したものを記載しています。基礎的な実行例に興味がある方は、[Plot API Example](https://moshi4.github.io/pyCirclize/plot_api_example/)ドキュメントと併せて参照するとpyCirclizeに実装されているプロット機能の理解が深まると思います。

### セクター・トラックのレイアウト可視化

```python
from pycirclize import Circos

# Initialize circos sectors
sectors = {"A": 10, "B": 15, "C": 12, "D": 20, "E": 15}
circos = Circos(sectors, space=5)

for sector in circos.sectors:
    # Plot sector axis & name text
    sector.axis(fc="none", ls="dashdot", lw=2, ec="black", alpha=0.5)
    sector.text(f"Sector: {sector.name}={sector.size}", size=15)
    # Set Track01 (Radius: 75 - 100)
    track1 = sector.add_track((75, 100))
    track1.axis(fc="tomato", alpha=0.5)
    track1.text(track1.name)
    # Set Track02 (Radius: 45 - 70)
    track2 = sector.add_track((45, 70))
    track2.axis(fc="cyan", alpha=0.5)
    track2.text(track2.name)
    # Set Track03 (Radius: 15 - 40)
    track3 = sector.add_track((15, 40))
    track3.axis(fc="lime", alpha=0.5)
    track3.text(track3.name)

circos.savefig("basic_example01.png")
```

![basic_example01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/5f1e17a2-1b91-73e0-ced1-87e54e6d0372.png)

### トラックへのデータプロット

```python
from pycirclize import Circos
import numpy as np
np.random.seed(0)

sectors = {"A": 10, "B": 15, "C": 12, "D": 20, "E": 15}
circos = Circos(sectors, space=5)
for sector in circos.sectors:
    # Plot sector axis & name text
    sector.text(f"Sector: {sector.name}", r=110, size=15)
    # Create x positions & randomized y values for data plotting
    x = np.arange(sector.start, sector.end) + 0.5
    y = np.random.randint(0, 100, len(x))
    # Plot line
    line_track = sector.add_track((75, 100), r_pad_ratio=0.1)
    line_track.axis()
    line_track.xticks_by_interval(1)
    line_track.line(x, y)
    # Plot points
    points_track = sector.add_track((45, 70), r_pad_ratio=0.1)
    points_track.axis()
    points_track.scatter(x, y)
    # Plot bar
    bar_track = sector.add_track((15, 40), r_pad_ratio=0.1)
    bar_track.axis()
    bar_track.bar(x, y)

circos.savefig("basic_example02.png")
```

![basic_example02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/fd00db49-ab28-e5ca-1fee-c04b5b646dc1.png)

### データ内・データ間リンクのプロット

```python
from pycirclize import Circos

sectors = {"A": 10, "B": 20, "C": 15}
name2color = {"A": "red", "B": "blue", "C": "green"}
circos = Circos(sectors, space=5)
for sector in circos.sectors:
    track = sector.add_track((95, 100))
    track.axis(fc=name2color[sector.name])
    track.text(sector.name, color="white", size=12)
    track.xticks_by_interval(1)

# Plot links in various styles
circos.link(("A", 0, 1), ("A", 7, 8))
circos.link(("A", 1, 2), ("A", 7, 6), color="skyblue")
circos.link(("A", 9, 10), ("B", 4, 3), direction=1, color="tomato")
circos.link(("B", 5, 7), ("C", 6, 8), direction=1, ec="black", lw=1, hatch="//")
circos.link(("B", 18, 16), ("B", 11, 13), r1=90, r2=90, color="violet", ec="red", lw=2, ls="dashed")
circos.link(("C", 1, 3), ("B", 2, 0), direction=1, color="limegreen")
circos.link(("C", 11.5, 14), ("A", 4, 3), direction=2, color="chocolate", ec="black", lw=1, ls="dotted")

circos.savefig("basic_example03.png")
```

![basic_example03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/6405b061-e9ba-d9bc-61f4-7bfe6a0579d3.png)

## 実践

実践例のソースコードは長くなるので、ここには記載しません。代わりにドキュメントへのリンクをそれぞれ貼ってあるので、興味がある方はそちらからソースコードを参照してください。

### Chord Diagram例 ①

下記の行列データを入力として、Chord Diagramをプロット ([ドキュメント](https://moshi4.github.io/pyCirclize/chord_diagram/#1-3-example3-10-x-2))

```text
     K    L
A   83   79
B   90  118
C  165   81
D  121   77
E  187  197
F  177    8
G  141  127
H   29   27
I   95   82
J  107   39
```

![chord_diagram1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/5fe9363d-f2e1-3a4d-1feb-2ffe7f7f33cc.png)

### Chord Diagram例 ②

下記の行列データを入力として、Chord Diagramをプロット ([ドキュメント](https://moshi4.github.io/pyCirclize/chord_diagram/#1-2-example2-10-x-10))

```text
     A    B    C    D    E    F    G    H    I    J
A   51  115   60   17  120  126  115  179  127  114
B  108  138  165  170   85  221   75  107  203   79
C  108   54   72  123   84  117  106  114   50   27
D   62  134   28  185  199  179   74   94  116  108
E  211  114   49   55  202   97   10   52   99  111
F   87    6  101  117  124  171  110   14  175  164
G  167   99  109  143   98   42   95  163  134   78
H   88   83  136   71  122   20   38  264  225  115
I  145   82   87  123  121   55   80   32   50   12
J  122  109   84   94  133   75   71  115   60  210
```

![chord_diagram2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/63bab3d6-d4f8-6519-3ca0-4e22f3a0dc6f.png)

___
::: note info
バイオインフォマティクス分野のデータ可視化に馴染みがない方は、以降の図の意味を理解するのは難しいと思います。ただpyCirclizeによる可視化の柔軟性を示す例として参考にはなるかと思います。
:::

### バクテリオファージゲノム

GFFファイルを入力として、腸内細菌を宿主とするバクテリオファージ(*Enterobacteria phage*)のゲノム特徴を可視化 ([ドキュメント](https://moshi4.github.io/pyCirclize/circos_plot/#1-enterobacteria-phage))

![phage_feature.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/d854fded-daf4-8012-6563-37ba325dd248.png)

### 大腸菌ゲノム

Genbankファイルを入力として、大腸菌(*Escherichia coli*)の各種ゲノム特徴を可視化 ([ドキュメント](https://moshi4.github.io/pyCirclize/circos_plot/#2-escherichia-coli))

![ecoli_features.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/ae6ca7b6-2760-1533-2952-238d48927730.png)

### ヒトゲノム

[UCSC](https://genome.ucsc.edu/cgi-bin/hgTables)の各種ヒトゲノムデータ(hg38)を入力として、ゲノム特徴及び重複セグメントの一部をリンクとして可視化 ([ドキュメント](https://moshi4.github.io/pyCirclize/circos_plot/#3-homo-sapiens-hg38))

![hg38_chr_link.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/356f1dbb-1b08-ad7f-7e2b-0eddf396e8fc.png)

### 系統樹

[Ensemblデータベース](https://asia.ensembl.org/info/about/speciestree.html)の脊椎動物(*Vertebrates*)における種の系統樹を色付きで可視化 ([ドキュメント](https://moshi4.github.io/pyCirclize/phylogenetic_tree/#2-large-tree))

![ensembl_tree.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138429/f46b826a-10a5-8d3b-8142-7a5b39f644fe.jpeg)

# 開発で苦労した点

振り返り・備忘録も兼ねて開発で苦労した点を記載します。

## 極座標(Polar)プロット

今回のpyCirclizeの開発で初めて一からまともにmatplotlibの[極座標(Polar)プロット機能](https://matplotlib.org/stable/gallery/pie_and_polar_charts/polar_demo.html)を触ってみました。XY直交座標系とはデータの取り扱いが当然異なるのでかなり戸惑いました。`度数(Degree)`を`弧度(Radian)`へ変換したり、セクターやトラック内データの(`X座標`, `Y座標`)を(`弧度(Radian)座標`, `半径(Radius)座標`)へ変換したりと`直交座標 -> 極座標`で都度座標の変換が必要だったのが、数学が苦手な自分にはなかなかに面倒な作業でした。コーディング中に「今どの位置の何の座標系のデータを取り扱っているんだっけ？？」と混乱しながら何とか実装していました。

## 描画速度の効率化

matplotlibで大量データのプロットを適当に実装すると地味に描画時間がかかることがあります。pyCirclizeでは以下の実装方針を取ることで描画速度を可能な限り効率化することを試みました。

- **Collectionを利用した一括描画**
開発当初は描画API呼び出し毎に、逐次プロットしていく方法をとっていましたが、それでは求める描画速度に到達しませんでした。matplotlibではPatch(図形の基底クラス)に属するオブジェクトをCollectionとして一括描画することで、逐次プロットするより描画速度を効率化できる仕組みがあります([参考](https://matplotlib.org/stable/api/collections_api.html))。そこで、描画API呼び出し毎にPatchオブジェクトを生成・収集し、最後に[PatchCollection](https://matplotlib.org/stable/api/collections_api.html#matplotlib.collections.PatchCollection)としてまとめて描画する仕様に変更した。

この効率化を実現するために本来は簡単な描画処理も少し煩雑に実装する必要が出てきて、開発に苦労する一因になりました。ただこの効率化のおかげで、ある程度大きなデータでもそこそこ速く描画はできるようになったと思います。

## ドキュメント作成

**開発で苦労「した」点**、というよりも**現在も苦労「している」点**です。
pyCirclizeには様々なプロット機能を実装していますが、全ての機能詳細をドキュメントとして文章化するのはかなり面倒で難しい作業です。2022年12月現在では、とりあえず実行例を羅列しているだけの感じになっている[ドキュメントサイト](https://moshi4.github.io/pyCirclize)をGitHub Pages上で[mkdocs](https://squidfunk.github.io/mkdocs-material/)を利用して作成・公開しています。今後、もうちょっと良い感じのドキュメントに仕上げられるといいなと思います。

# 最後に

pyCirclizeをとりあえず公開してみても良いかなと思えるレベルまでは開発してみましたが、正直circlizeの機能・ドキュメントの豊富さにはまだまだ遠く及びません。circlizeは2014年くらいから継続的に開発されている洗練されたRパッケージなので、開発期間2ヶ月程度のpyCirclizeが及ばないのは当然ではあるのですが。。。
ただpyCirclizeも基本的な円形データ可視化を実行する上で必要な機能は一応実装できているかなと思います。またPythonで円形データ可視化が簡単かつ柔軟にできることに意味があるとも考えているので、興味がある方はぜひ実際に触ってみてください。
pyCirclizeが「いいね！」と思う方がいたら、GitHubでStarを付けてもらえると嬉しいです。

https://github.com/moshi4/pyCirclize