個人的なグラフ描画用のパッケージ作りはじめた
個人的なグラフ描画用のパッケージ作りはじめた。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)
pythonのastモジュールに不満がでたらlib2to3のコードを使ってみよう
はじめに
pythonのコードをparseするためにastモジュールが用意されていたりする。 このモジュールはpythonのコードをparseしてvisitor的なものでastをtravarseしてなどと便利ではあるのだけれど。 コメントなどの情報が消えてしまうなどの不満が出ることがある。 このような時にlib2to3用のコードを使ってみると良いのではないかという話。
lib2to3?
2to3というツールがあったりする。これはpython2.x用のコードをpython3.x用のコードに変換してくれるツール。 よく考えてみて欲しいのだけれど、2to3によってコメントの情報が失われることはない。そして2to3もおおよそASTを取り出してからの変換ということになっているはず。ということは2to3の内部のコードを覗いてみればコメントなどの情報を失うことなくAST変換を行う術が分かるはず。
何が言いたいかというと、コメント情報などの失われを防ぐためにlexerなどから作るなどということは不要ということ(ちなみに完全にフルでparserを再実装したbaronというものもあったりする。ただもう少し抽象度の高いredbaronから使う事がおすすめされていたりする)。
そして2to3の内部で使われているコードがlib2to3というもの。ちなみにこのlib2to3はyapfというコードフォーマッター(gofmtのようなもの)にも使われていたりする。
ちょっとしたコード変換
試しにlib2to3を利用してちょっとしたコード変換をしてみる。
例えば以下の様なコードがあるとする。
hello.py
def hello(): # this is comment return "hello"
これを以下の様に変換してみる。
def *replaced*(): # this is *replaced* comment return "hello"
こういう感じのコードを書けば良い。
from lib2to3 import pytree from lib2to3 import pygram from lib2to3.pgen2 import driver default_driver = driver.Driver(pygram.python_grammar_no_print_statement, convert=pytree.convert) def parse(code, parser_driver=default_driver): return parser_driver.parse_string(code, debug=True) with open("hello.py") as rf: t = parse(rf.read()) print(t) t.children[0].children[1].value = "*replaced*" t.children[0].children[4].children[1].prefix = " # this is *replaced* comment\n" print(t)
treeを直接触っているので何をやっているかはものすごく分かりづらいものではあるけれど。テキトウに関数名やコメント部分に *replaced*
という文字列を挿入している。tree自体を文字列として出力するとおおよそそのままpythonコードとして出力されるというのも便利。
ちなみにファイルからtreeを作る際は以下でも良い。
t = default_driver.parse_file("hello.py")
もう少し真面目にするなら
もう少し真面目にするなら、このlib2to3用のASTに対するvisitorを作ってあげると良い。 yapfのpytree_visitor.pyなどが参考になる。
もう少し詳しいことは気が向いたら書くかもしれない。