個人的なグラフ描画用のパッケージ作りはじめた
個人的なグラフ描画用のパッケージ作りはじめた。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
でも良い。
上のコードと同様のコードを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--')
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
あと
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)