個人的なグラフ描画用のパッケージ作りはじめた

github.com

個人的なグラフ描画用のパッケージ作りはじめた。matplotlibのラッパーの様なもの。 元々のモチベーションは以下の記事に書いてある。

結局グラフの表示用のコードとグラフを画像として出力する用のコードが異なるのがつらい感じだった。

コンセプト的な何か

作ったもののコンセプト的な何かは以下のようなもの

  • グラフを表示することとグラフを画像として出力する事がシームレスに行えて欲しい
  • 個人的な初期設定がデフォルトで入っていて欲しい

後々これに加えて以下も加わった。

  • なるべくグラフの表示位置が変わるなら(e.g. subplot)コードとしての見た目の位置も変わって欲しい
  • pytestのyield_fixture的な準備の機能

グラフを表示することとグラフを画像として出力する事がシームレスに行えて欲しい

これは冒頭の説明と同様のもの。描画したグラフを表示させてみて良さそうと思ったら画像として出力したい。 さすがに誰かに作ったグラフを共有するためだけに、グラフを画面に表示させた状態でスクリーンショット取るなどということは避けたい。

こんな感じのコードで大丈夫。

00simple.py

from utatane import as_command


@as_command
def main(plt):
    xs = list(range(10))
    ys = [x * x for x in xs]
    plt.plot(xs, xs, "g", label="x")
    plt.plot(xs, ys, "b", label="x * x")

as_command というデコレータがつくと自動的にコマンドになる。それぞれshow,dumpというサブコマンドが存在する。

$ python 00simple.py -h
usage: 00simple.py [-h] {dump,show} ...

positional arguments:
  {dump,show}

optional arguments:
  -h, --help   show this help message and exit

グラフの表示

デフォルトではshowが実行される。もちろん明示的に python <filename> show でも良い。

00simple.png

上のコードと同様のコードをpyplotだけで描くと以下と同じもの。

import matplotlib.pyplot as plt
plt.style.use("ggplot")

xs = list(range(10))
ys = [x * x for x in xs]
plt.plot(xs, xs, "g", label="x")
plt.plot(xs, ys, "b", label="x * x")

plt.legend()
plt.show()

テーマは常にggplotで良い気がするので自動的に選ばれる様になっている(どうせ個人的な用途なので。毎回書くのが面倒くさくなった)。 plt.show() は不要。

グラフを画像として出力

グラフを画像として出力するにはdumpを使えば良い。名前は変えるかもしれない。

$ python 00simple.py dump /tmp/00simple.svg
save: /tmp/00simple.svg
$ identity
/tmp/00simple.svg SVG 720x540 720x540+0+0 16-bit sRGB 28.3KB 0.000u 0:00.000

ちなみに--width--heightでサイズは変えられる。

$ python 00simple.py dump --width 400 --height 300 /tmp/00simple2.svg
save: /tmp/00simple2.svg
$ identity /tmp/00simple2.svg
/tmp/00simple2.svg SVG 450x338 450x338+0+0 16-bit sRGB 28.1KB 0.000u 0:00.000

(計算にバグがあるっぽい)

グラフの表示位置と連動してコード上でも位置が変わって欲しい

subplot

例えば1枚の図の中に2つのグラフを描くときに、どこからどこまでが1つ目のグラフのものでどこからどこまでが2つ目のグラフが分かるようにしたかった。

こんな感じ。

import numpy as np
from utatane import as_command, subplot


def f(t):
    return np.exp(-t) * np.cos(2 * np.pi * t)


@as_command
def main(plt):
    t1 = np.arange(0.0, 5.0, 0.1)
    t2 = np.arange(0.0, 5.0, 0.02)

    with subplot(plt, nrows=2, ncols=1) as nth:
        with nth(1):
            plt.plot(t1, f(t1), 'bo', t2, f(t2), 'k')

        with nth(2):
            plt.plot(t2, np.cos(2 * np.pi * t2), 'r--')

02multiple_figure.png

nthの部分で2つのグラフがあることが分かる。ちなみにこれと同様のコードがpyplotだけの場合は以下の様な感じになる(一部分だけ)。

plt.subplot(211)
plt.plot(t1, f(t1), 'bo', t2, f(t2), 'k')
plt.subplot(212)
plt.plot(t2, np.cos(2*np.pi*t2), 'r--')

これはpyplotのチュートリアルから持ってきたもの。

このコード自体は4行に過ぎないのででたくさんコードが必要になってつらいというわけではないのだけれど。pyplotは状態を抱えてしまっているのでコードが長くなってくると、結構ていねいに内部的な状態を把握しながら読んでいかなければならなくなってくる。徐々につらくなってくる。

なので状態を持たない方向に変更していきたいとは思ったりはしているものの、とは言え、まだ、pyplotから完全に独立できたというわけでもない。普通にpyplotが内部で使われているので結局やっている事は同じ。

これを作っていて思ったけれど。jupyter notebookなどで試すときにはインデントが少ない方が書きやすいという気持ちが出る気がする。 一方で、通常のエディタ上で書く一繋ぎのコードに関しては、適宜インデントなどで見た目的にも区切った形にしたいという気持ちが芽生えたりするみたい。

あと、初見で211と212という謎のマジックナンバー的な値の意味を把握するのは難しい気がした(慣れれば楽ではあるけれど)。

multiple windows (figures)

あとそれ以外に図を複数表示したい場合にも(1つの画像中に複数のグラフではなく、複数の画像を一気に作成ということ)。

04multi_window.py

from utatane import as_command, window


@as_command
def main(plt):
    xs = range(100)

    with window(plt, 0):
        plt.plot(xs, xs)

    with window(plt, 1):
        plt.plot(xs, [x * x for x in xs])

今度は2つのwindowが表示される形。これもpyplotだけの場合にはインデントがない形になる。

xs = range(100)

plt.figure(0)
plt.plot(xs, xs)

plt.figure(1)
plt.plot(xs, [x * x for x in xs])

好みの問題かもしれない。

もちろん、これを画像として出力する場合には2枚の画像が出力される。

$ python 04multi_window.py dump /tmp/04multi_window.svg
save: /tmp/04multi_window0.svg
save: /tmp/04multi_window1.svg

04multi_window0.png 04multi_window1.png

あと

3D表示もできるらしいのでやってみた。意外とグラフを描き始めてみると面白いかもしれない。

import numpy as np
from utatane import as_command, plot3d
from matplotlib import cm


def func(x, y):
    return x ** 2 + y ** 2


@as_command
def main(plt):
    x = np.arange(-5, 5, 0.05)
    y = np.arange(-5, 5, 0.05)

    X, Y = np.meshgrid(x, y)

    Z = func(X, Y)

    with plot3d(plt) as ax:
        ax.plot_surface(X, Y, Z, cmap=cm.coolwarm)

06plotting_3d_surface.png