matplotlib で描画する度に色をグラデーション的に変える（2次元 ver もあるよ！）

この記事は[古川研究室 AdventCalendar](https://qiita.com/flab5420/items/fc94dadc49635f0e5c9f) 4日目の記事です．
本記事は古川研究室の学生が学習の一環として書いたものです．
内容が曖昧であったり表現が多少異なったりする場合があります．

# まえがき

↓こういうことができるような実装を紹介します．
![star.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199099/236b875b-72e1-f111-0c30-8f80e6991340.png)


matplotlib で描画関数を呼び出す度に
色をグラデーション的に変えたいと思ったことはありますでしょうか．
私にはあります．

今回はその方法について書かせていただこうと思います．
何番煎じかわかりませんが，
様々な記事を参考にさせていただき
個人的に好みな実装にたどり着いたので投稿しようと思った次第です．

この記事を読んでくれた方には
ぜひ[参考文献](#参考文献)にある記事とも比較して
どの方法を採用するかを決めていただけたらと思っております．
また，本記事には個人的な好みが散りばめられていることをご了承ください．

お急ぎの方は[実装例](#実装例)までジャンプしてもらって OK です．

# matplotlib

matplotlib は言わずと知れた Python の描画ライブラリです．
静的なグラフはもちろん，アニメーションやマウス操作などによるインタラクションなども
包括的にサポートしています．

matplotlib の機能として，
`plot` や `scatter` などの描画関数を複数回呼び出す時に
グラフの色を変えてくれるというものがあります[^1]．
デフォルトでは 青→オレンジ→緑→・・・のように変化します．
本記事のゴールは，これをグラデーション的は配色に変えることです．

# デフォルトの配色はどうなっているのか

デフォルトの配色はどうなっているかという話は，
下のページに詳しく書かれています．

https://matplotlib.org/stable/users/dflt_style_changes.html

![newcolors.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199099/4ecd1d88-4ab4-2796-1b9a-b216076366f7.png)
（公式サイトより抜粋）

デフォルトでは，描画関数を複数回呼び出すと`#1f77b4`（青）→`#ff7f0e`（オレンジ）→`#2ca02c`（緑）→...のように色が変化します（上図の上から順に）[^3]．
これらの色には 'cyan' など特別な名前が付いているわけではありません．
この配色や順番は，視認性の観点から非常によく練られています[^2]．

デフォルトの配色も非常に良いのですが，
「どういう順番になっているか」という情報が欲しいときには少し不便です．
まず，配色からどういう順番になっているかを読み取るには色の順番を把握する必要があります．
また，慣れていれば色の順番はわかってくるものですが，わかっていたとしても
描画数が多くなるとごちゃごちゃして視認性が悪くなってしまいます．

例えば，$$y_i = \sin(x) + \frac{i}{10}, i \in \left\\{0, 1, \dots, 29 \right\\}, x\in \[ -\pi, + \pi \]　$$
のように，縦方向に $0.1$ ずつシフトしたサインカーブを 30 個ほど描画すると下図ようになります．


![default.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199099/db58eac2-527a-75c6-8886-5ff01150bf77.png)

下から上に連続的に変化していることが見えづらい上に，目がチカチカしてしまいますね．
そこで，描画する度にグラデーション的に配色を変えたいというモチベーションが湧きました．

# How to
どうやってグラデーションにするのかの方針を立てます．
コード全体は[実装例](#実装例)にあるので参考にされるのであればそちらをおすすめします．

## Step 1. 色のリストを作成

まず色のリストを作成します．
一般的なのは`matplotlib.cm`にある ColorMap を用いる方法かと思います．
色の指定は RGB のタプルでも可能ですので自作するのもアリです．

```Python:ColorMapを用いる場合
# Cool なグラデーションになります．
colors = [ mpl.cm.cool(i) for i in np.linspace(0, 1, 10, endpoint=False) ]
```

```Python:自作する例
from colorsys import hsv_to_rgb

# 色相を 0~1 に変化させることで虹色のグラデーションになります．
colors = [ hsv_to_rgb(h, 0.9, 1.0) for h in np.linspace(0, 1, 10, endpoint=False) ]
```

## Step 2. Axes オブジェクトに反映

次は配色のリストをどうやって反映させるのかという話になります．
公式ドキュメントには，以下のようにグローバルに設定する方法が紹介されていました．

```Python:公式のサンプル
from cycler import cycler
mpl.rcParams['axes.prop_cycle'] = cycler(color='bgrcmyk')
```

個人的にプログラム全体に影響を及ぼすことは避けたいため，
[Axesクラスの資料](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.set_prop_cycle.html)にもありますように，`matplotlib.axes.Axes.set_prop_cycle` を用いて，
以下のように`Axes` オブジェクトごとに配色を設定したいと思います．

```Python:私はこっち派
fig = plt.figure(figsize=(7, 7))
ax = fig.add_subplot(111)
ax.set_prop_cycle(color=colors)  # Step 1 で定義した colors
```

`colors`自体はリストですが，
`matplotlib.axes.Axes.set_prop_cycle` の [内部](https://github.com/matplotlib/matplotlib/blob/5b87c98e07be94d05a7a5fc2e58b92554e210186/lib/matplotlib/axes/_base.py#L239)で`itertools.cycle`に変換してくれているので
公式のサンプルの`cycler` を用いたときと同様，
色リストの終端に来たら先頭に戻るような挙動をしてくれます．

# 実装例

先程の30個のサインカーブを
デフォルトの配色で描画した場合とグラデーションで描画した場合とで比較します．

```Python:sin.py
import numpy as np
import matplotlib as mpl
from matplotlib import pyplot as plt


if __name__ == '__main__':
    # Making multiple sine curves.
    num_tasks = 30
    num_samples = 1000
    x = np.linspace(-np.pi, np.pi, num_samples)
    Y = np.empty((num_tasks, num_samples))
    for i in range(num_tasks):
        Y[i] = np.sin(x) + 0.1*i

    # Making list of colors.
    colors = [ mpl.cm.cool(i) for i in np.linspace(0, 1, num_tasks, endpoint=False) ]

    # Drawing sine curvies.
    fig = plt.figure(figsize=(7, 7))
    ax = fig.add_subplot(111)
    ax.set_aspect('equal')

    ax.set_prop_cycle(color=colors)  # Setting color cycle !
    for y in Y:
        ax.plot(x, y)
    plt.show()
```

実行結果はこちら
![sin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199099/070fda73-526f-8c3d-052c-a66e99e52aa4.png)

Cool なグラデーションで見やすくなっています．
`plot` 関数の引数で色を指定する必要はなく，自動で色を指定してくれます．

# おまけ：2次元のグラデーション

色のリストを自作することで，2次元のグラデーションも実現できそうだったので書いてみました．
HSV色空間の**H（色相）**と**V（明度）**を変化させるサンプルを以下に示します．

```Python:star.py
import numpy as np
import matplotlib as mpl
from matplotlib import pyplot as plt
from itertools import product
from colorsys import hsv_to_rgb


def make_grid(resolution, domain):
    mesh = np.linspace(domain[0], domain[1], resolution)
    return np.array(list(product(mesh, repeat=2)))


def gen_star_dots(center, radius):
    thetas = (np.linspace(0, 4, 6, endpoint=True) + 0.1) * np.pi
    x = radius * np.cos(thetas) + center[0]
    y = radius * np.sin(thetas) + center[1]
    return np.concatenate([x[:, np.newaxis], y[:, np.newaxis]], axis=1)


if __name__ == '__main__':
    # Making multiple stars (7*7).
    resolution = 7
    grid = make_grid(resolution, domain=(0, 1))
    X = np.empty((resolution**2, 6, 2))
    for i, center in enumerate(grid):
        X[i] = gen_star_dots(center, radius=0.05)

    # Making list of colors (7*7 × RGB).
    colors = [
        hsv_to_rgb(h, 0.8, v) for h, v in make_grid(resolution, domain=(0.3, 1))
    ]

    # Drawing sine stars.
    fig = plt.figure(figsize=(7, 7))
    ax = fig.add_subplot(111)
    ax.set_aspect('equal')
    ax.set_prop_cycle(color=colors)  # Setting color cycle !
    for x in X:
        ax.plot(x[:, 0], x[:, 1], lw=3)
    plt.show()
```
`make_grid` 関数では，2次元平面でグリッド状に座標を振っています．
また `gen_star_dots`関数では `center` の座標を中心に，星の形になるように点群を生成しています．

実行結果はこちら
![star.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199099/236b875b-72e1-f111-0c30-8f80e6991340.png)

2次元のグラデーションも比較的簡単にできます．
デフォルトの配色だと無秩序な感じになってしまっていますね[^4]．
今回の例に限らず，配色を上手く利用することで
視認性を上げたり，場合によっては得られる情報を増やしたりすることができます．

# おわりに

私が本記事にある内容を調べようと思った理由は，
研究で「マルチ多様体モデリングによるメタ学習」を扱う際に，
学習結果を描画する際に "タスクごとに" プロットする色をグラデーション的に変えたいとなったからです．

「マルチ多様体モデリングによるメタ学習」とは何ぞやと興味を持っていただいた方は
是非以下のリンクをご参照ください（宣伝）．

http://www.brain.kyutech.ac.jp/~furukawa/myresearch.html

# 参考にさせていただいた文献
- [matplotlibでcolor cycleのN番目の色を指定するいくつかの方法（Qiita）](https://qiita.com/skotaro/items/5c9893d186ccd31f459d)
- [matplotlibで色をグラデーション的に選択（Qiita）](https://qiita.com/hokekiyoo/items/cea310b2c36a01b970a6)
- [matplotlibで色をグラデーションにする（Qiita）](https://qiita.com/ShoheiKojima/items/6705ce31d6cffb64daff)
- [matplotlibのcolor cycleをいい感じにしてみる（Qiita）](https://qiita.com/okd46/items/5940b317cd4ef5dd2dd8)
- [Changes to the default style（matplotlib）](https://matplotlib.org/stable/users/dflt_style_changes.html)
- [matplotlib.axes.Axes.set_prop_cycle（matplotlib）](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.set_prop_cycle.html)
- [Choosing Colormaps in Matplotlib（matplotlib）](https://matplotlib.org/stable/tutorials/colors/colormaps.html)

[^1]: 同じ描画関数を複数回呼び出した時に色が変化します．`plot`→`scatter`のように呼び出しても色は変わりません． 
[^2]: 公式が出している記事で見た記憶がありますが，時間の関係でソースにはたどり着けませんでした.
[^3]: 色のサンプルが見れるの凄い．
[^4]: これはこれでキレイだという意見もあるかも知れませんが，そういう話ではありません．