CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: matplotlib で動的にグラフを生成する

今回は matplotlib を使って動的にグラフを生成する方法について。 ここでいう動的というのは、データを逐次的に作って、それを随時グラフに反映していくという意味を指す。 例えば機械学習のモデルを学習させるときに、その過程 (損失の減り方とか) を眺める用途で便利だと思う。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77
$ python -V
Python 3.6.5
$ pip list --format=columns | egrep -i "(matplotlib|pillow)"
matplotlib      2.2.2  
Pillow          5.2.0  

もくじ

下準備

まずは今回使うパッケージをインストールしておく。

$ pip install matplotlib pillow

静的にグラフを生成する

動的な生成について説明する前に、まずは静的なグラフの生成から説明する。 といっても、これは一般的な matplotlib のグラフの作り方そのもの。 あらかじめ必要なデータを全て用意しておいて、それをグラフとしてプロットする。

この場合、当たり前だけどプロットする前に全てのデータが揃っていないといけない。 例えば機械学習なら、モデルの学習を終えて各エポックなりラウンドごとの損失が出揃っている状態まで待つ必要がある。

次のサンプルコードではサイン波のデータをあらかじめ作った上で、それを折れ線グラフにしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math

import numpy as np
from matplotlib import pyplot as plt


def main():
    # 描画領域
    fig = plt.figure(figsize=(10, 6))
    # 描画するデータ
    x = np.arange(0, 10, 0.1)
    y = [math.sin(i) for i in x]

    # グラフを描画する
    plt.plot(x, y) 

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

上記を適当な名前でファイルに保存して実行してみよう。

$ python sin.py

すると、次のようなグラフが表示される。

f:id:momijiame:20180712235912p:plain

これが静的なグラフ生成の場合。

動的にグラフを生成する

続いて動的にグラフを生成する方法について。 これには matplotlib.animation パッケージを使う。 特に FuncAnimation を使うと作りやすい。

matplotlib.animation — Matplotlib 3.1.1 documentation

次のサンプルコードでは、先ほどの例と同じサイン波を動的に生成している。 ポイントは、グラフの再描画を担当する関数をコールバックとして FuncAnimation に登録すること。 そうすれば、あとは FuncAnimation が一定間隔でその関数を呼び出してくれる。 呼び出されるコールバック関数の中でデータを生成したりグラフを再描画する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation


def _update(frame, x, y):
    """グラフを更新するための関数"""
    # 現在のグラフを消去する
    plt.cla()
    # データを更新 (追加) する
    x.append(frame)
    y.append(math.sin(frame))
    # 折れ線グラフを再描画する
    plt.plot(x, y)


def main():
    # 描画領域
    fig = plt.figure(figsize=(10, 6))
    # 描画するデータ (最初は空っぽ)
    x = []
    y = []

    params = {
        'fig': fig,
        'func': _update,  # グラフを更新する関数
        'fargs': (x, y),  # 関数の引数 (フレーム番号を除く)
        'interval': 10,  # 更新間隔 (ミリ秒)
        'frames': np.arange(0, 10, 0.1),  # フレーム番号を生成するイテレータ
        'repeat': False,  # 繰り返さない
    }
    anime = animation.FuncAnimation(**params)

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

先ほどと同じようにファイルに保存したら実行する。

$ python sin.py

すると、次のように動的にグラフが描画される。

f:id:momijiame:20180712235931g:plain

ちなみに、上記のような GIF 画像や動画は次のようにすると保存できる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation


def _update(frame, x, y):
    """グラフを更新するための関数"""
    # 現在のグラフを消去する
    plt.cla()
    # データを更新 (追加) する
    x.append(frame)
    y.append(math.sin(frame))
    # 折れ線グラフを再描画する
    plt.plot(x, y)


def main():
    # 描画領域
    fig = plt.figure(figsize=(10, 6))
    # 描画するデータ (最初は空っぽ)
    x = []
    y = []

    params = {
        'fig': fig,
        'func': _update,  # グラフを更新する関数
        'fargs': (x, y),  # 関数の引数 (フレーム番号を除く)
        'interval': 10,  # 更新間隔 (ミリ秒)
        'frames': np.arange(0, 10, 0.1),  # フレーム番号を生成するイテレータ
        'repeat': False,  # 繰り返さない
    }
    anime = animation.FuncAnimation(**params)

    # グラフを保存する
    anime.save('sin.gif', writer='pillow')


if __name__ == '__main__':
    main()

グラフを延々と描画し続ける

先ほどの例では frames オプションに渡すイテレータに終わりがあった。 具体的には 0 ~ 10 の範囲を 0.1 区切りで分割した 100 のデータに対してグラフを生成した。 また repeat オプションに False を指定することで繰り返し描画することも抑制している。

続いては、先ほどとは異なり frames オプションに終わりのないイテレータを渡してみよう。 こうすると、手動で止めるかメモリなどのリソースを食いつぶすまでは延々とデータを生成してグラフを描画することになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import itertools
import math

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation


def _update(frame, x, y):
    """グラフを更新するための関数"""
    # 現在のグラフを消去する
    plt.cla()
    # データを更新 (追加) する
    x.append(frame)
    y.append(math.sin(frame))
    # 折れ線グラフを再描画する
    plt.plot(x, y)


def main():
    # 描画領域
    fig = plt.figure(figsize=(10, 6))
    # 描画するデータ
    x = []
    y = []

    params = {
        'fig': fig,
        'func': _update,  # グラフを更新する関数
        'fargs': (x, y),  # 関数の引数 (フレーム番号を除く)
        'interval': 10,  # 更新間隔 (ミリ秒)
        'frames': itertools.count(0, 0.1),  # フレーム番号を無限に生成するイテレータ
    }
    anime = animation.FuncAnimation(**params)

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

上記をファイルに保存して実行してみよう。

$ python sin.py

ずーーーっとグラフが生成され続けるはず。

f:id:momijiame:20180715153531p:plain

Jupyter Notebook 上で動的にグラフを生成する

Jupyter Notebook 上で動的なグラフ生成をするときは、次のように %matplotlib nbagg マジックコマンドを使う。 また、意図的に pyplot.show() を呼び出す必要はない。

%matplotlib nbagg

import itertools
import math

import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation


def _update(frame, x, y):
    """グラフを更新するための関数"""
    # 現在のグラフを消去する
    plt.cla()
    # データを更新 (追加) する
    x.append(frame)
    y.append(math.sin(frame))
    # 折れ線グラフを再描画する
    plt.plot(x, y)


# 描画領域
fig = plt.figure(figsize=(10, 6))
# 描画するデータ
x = []
y = []

params = {
    'fig': fig,
    'func': _update,  # グラフを更新する関数
    'fargs': (x, y),  # 関数の引数 (フレーム番号を除く)
    'interval': 10,  # 更新間隔 (ミリ秒)
    'frames': np.arange(0, 10, 0.1),  # フレーム番号を生成するイテレータ
    'repeat': False,  # 繰り返さない
}
anime = animation.FuncAnimation(**params)

データの更新間隔とグラフの再描画間隔をずらす

グラフの再描画はそこまで軽い処理でもないし、データの更新間隔とずらしたいときもあるかも。 そんなときはデータを更新するスレッドと、グラフを再描画するスレッドを分ける。

以下のサンプルコードではデータの更新用に新しくスレッドを起動している。 データの更新間隔が 100ms 間隔なのに対してグラフの再描画は 250ms 間隔にしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import threading
import math
import itertools

from matplotlib import pyplot as plt
from matplotlib import animation


def _redraw(_, x, y):
    """グラフを再描画するための関数"""
    # 現在のグラフを消去する
    plt.cla()
    # 折れ線グラフを再描画する
    plt.plot(x, y)


def main():
    # 描画領域
    fig = plt.figure(figsize=(10, 6))
    # 描画するデータ (最初は空っぽ)
    x = []
    y = []

    def _update():
        """データを一定間隔で追加するスレッドの処理"""
        for frame in itertools.count(0, 0.1):
            x.append(frame)
            y.append(math.sin(frame))
            # データを追加する間隔 (100ms)
            time.sleep(0.1)

    def _init():
        """データを一定間隔で追加するためのスレッドを起動する"""
        t = threading.Thread(target=_update)
        t.daemon = True
        t.start()

    params = {
        'fig': fig,
        'func': _redraw,  # グラフを更新する関数
        'init_func': _init,  # グラフ初期化用の関数 (今回はデータ更新用スレッドの起動)
        'fargs': (x, y),  # 関数の引数 (フレーム番号を除く)
        'interval': 250,  # グラフを更新する間隔 (ミリ秒)
    }
    anime = animation.FuncAnimation(**params)

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

上記をファイルに保存して実行してみよう。

$ python sin.py

これまでに比べるとグラフの再描画間隔が長いので、ちょっとカクカクした感じでグラフが更新される。

めでたしめでたし。