Common LispからGnuplotでグラフ表示するライブラリを作った: clgplot

オレオレユーティリティ集からプロット周りを切り出したのでgithubに置いてみる。
https://github.com/masatoi/clgplot
なおGnuplot4を必要とする。macで動くかは分からない。昔に書いたものなのでUIOPじゃなくてexternal-programを使っているところがアレである。
Gnuplotのフロントとしては似たようなのが沢山ある気がするが、Gnuplotを非同期に呼び出してコマンドを一つずつ渡していくというよりも、一気にGnuplotスクリプトに書き出してデータとともにGnuplotに渡すということがしたかったように思う。具体的には、リスト等の中身を/tmp/gnuplot-tmp.datに出力して、/tmp/gnuplot-tmp.gpにGnuplotスクリプトを書き出してpersistオプション付きでgnuplotを呼び出す。

インストール

cd ~/quicklisp/local-projects
git clone https://github.com/masatoi/clgplot.git
(ql:quickload :clgplot)

gnuplotにパスが通ってないとか一時ファイルのできる場所を変えたいとかいうときは変数を設定する必要があるかもしれない。以下はデフォルト値。

(in-package :clgplot)
(defparameter *gnuplot-path* "gnuplot")
(defparameter *tmp-dat-file* "/tmp/gnuplot-tmp.dat")
(defparameter *tmp-gp-file* "/tmp/gnuplot-tmp.gp")

使い方

まず、単にシーケンスを渡せばグラフが表示される(Qキーで閉じる)

(plot '(1 2 3))
;; または
(plot #(1 2 3))

(defparameter *x-list* (loop for i from (- pi) to pi by 0.1 collect i))
(plot (mapcar #'sin *x-list*))


何も指定しないと横軸はデータ番号になる。x軸に定義域を指定したいときは、x-seqキーワードオプションを使う。

(plot (mapcar #'sin *x-list*) :x-seq *x-list*)

複数グラフ

複数のグラフを描きたいときはplotsを使ってシーケンスのシーケンスを渡す。それに対応して定義域もシーケンスのシーケンスになり、x-seqsで指定する。

(plots (list (mapcar #'sin *x-list*)
             (mapcar #'cos *x-list*))
       :x-seqs (list *x-list* *x-list*))


ついでにtanも追加してみよう。

(plots (list (mapcar #'sin *x-list*)
             (mapcar #'cos *x-list*)
             (mapcar #'tan *x-list*))
       :x-seqs (list *x-list* *x-list* *x-list*))


表示範囲が自動調整されて見難くなってしまった。

表示範囲を指定

表示範囲を指定するにはx-range、y-rangeオプションを使う。

(plots (list (mapcar #'sin *x-list*)
             (mapcar #'cos *x-list*)
             (mapcar #'tan *x-list*))
       :x-seqs (list *x-list* *x-list* *x-list*)
       :x-range (list (- pi) pi)
       :y-range '(-1 1))

凡例、軸の説明を追加

説明が無いと寂しいので、title-listで凡例を、x-labelとy-labelで軸の説明を指定する。

(plots (list (mapcar #'sin *x-list*)
             (mapcar #'cos *x-list*)
             (mapcar #'tan *x-list*))
       :x-seqs (list *x-list* *x-list* *x-list*)
       :x-range (list (- pi) pi)
       :y-range '(-1 1)
       :title-list '("sin" "cos" "tan")
       :x-label "x"
       :y-label "f(x)")

ファイル出力

グラフが描けたので次はファイルに出力する。outputオプションでパスを指定すればそこに画像ファイルができる。
画像のフォーマットはデフォルトは640x480のPNGだが、output-formatオプションで他にも色々指定することができる。(:PDF :EPS :EPS-MONOCHROME :PNG :PNG-1280X1024 :PNG-2560X1024 :PNG-MONOCHROME が使える)
例えば、論文とかに貼り付けたいときはベクタ形式でモノクロのEPS-MONOCHROMEを指定する。など。

;; PNGファイル出力
(plots (list (mapcar #'sin *x-list*)
             (mapcar #'cos *x-list*)
             (mapcar #'tan *x-list*))
       :x-seqs (list *x-list* *x-list* *x-list*)
       :x-range (list (- pi) pi)
       :y-range '(-1 1)
       :title-list '("sin" "cos" "tan")
       :x-label "x"
       :y-label "f(x)"
       :output "/home/wiz/tmp/clgp-output.png")

;; フォーマット指定
(plots (list (mapcar #'sin *x-list*)
             (mapcar #'cos *x-list*)
             (mapcar #'tan *x-list*))
       :x-seqs (list *x-list* *x-list* *x-list*)
       :x-range (list (- pi) pi)
       :y-range '(-1 1)
       :title-list '("sin" "cos" "tan")
       :x-label "x"
       :y-label "f(x)"
       ;; :PDF :EPS :EPS-MONOCHROME :PNG :PNG-1280X1024 :PNG-2560X1024 :PNG-MONOCHROME
       :output-format :eps-monochrome
       :output "/home/wiz/tmp/clgp-output.eps")

スタイル

デフォルトではデータ点を繋ぐ線を表示するが、styleオプションによってデータ点を点として表示させたり、x軸からの距離(インパルス)を表示させることもできる。

(defparameter *x-list2*
  '(-3.14 -2.64 -2.14 -1.64 -1.14 -0.64 -0.14 0.358 0.858 1.358 1.858 2.358 2.858 3.14))

(plot (mapcar #'sin *x-list2*) :style 'line)
(plot (mapcar #'sin *x-list2*) :style 'points)
(plot (mapcar #'sin *x-list2*) :style 'impulse)


plot-listsではスタイルにリストを指定できるので、これらを組合せることができる。
例えば、sin関数の周りに正規分布からサンプリングしたデータ点を散らす場合はこうなる。

;; Box-Muller法による1次元正規分布のサンプリング
(defun random-normal (&key (mean 0d0) (sd 1d0))
  (let ((alpha (random 1.0d0))
	(beta  (random 1.0d0)))
    (+ (* sd
	  (sqrt (* -2 (log alpha)))
	  (sin (* 2 pi beta)))
       mean)))

(let* ((rand-x-list (loop repeat 100 collect (- (random (* 2 pi)) pi)))
       (rand-y-list (mapcar (lambda (x) (+ (sin x) (random-normal :sd 0.2d0))) rand-x-list)))
  (plots (list (mapcar #'sin *x-list*)
               rand-y-list)
         :x-seqs (list *x-list* rand-x-list)
         :style '(line point)))


3次元プロット

3次元の画像をプロットしたいときは、x軸とy軸の定義域リストと、z軸の値となる2変数関数を与える。例えば、z=sin(x)+cos(y)を表示したいときはこうする。

(splot (lambda (x y) (+ (sin x) (cos y)))
       *x-list*  ; x-list
       *x-list*) ; y-list


mapオプションに真の値を与えると上から見た図になる

(splot (lambda (x y) (+ (sin x) (cos y)))
       *x-list* ; x-list
       *x-list* ; y-list
       :map t)


人には行列(二次元配列)の値をプロットしたい状況というものがある。そんな時はsplot-matrix関数を使う。

(defparameter mat (make-array '(10 10)
                              :initial-contents
                              (loop for i from (- pi) to (- pi 0.1) by (/ pi 5) collect
                                (loop for j from (- pi) to (- pi 0.1) by (/ pi 5) collect
                                  (+ (sin i) (cos j))))))

(splot-matrix mat)


ヒストグラム

ヒストグラムはplot-histgramで表示する。第一引数はサンプルのリスト、第二引数がビンの数である。例えば標準正規分布から10000個サンプリングしてきてビン100個でプロットしてみるとこうなる。

(plot-histogram (loop repeat 100000 collect (random-normal)) ; samples
                100) ; number of bin


感想

ほとんどの機能をplotなどのキーワードオプションに集約させているので、使い方を覚えていなくてもSLIMEの引数補完でキーワードオプションを表示させれば大体分かるというのが気に入っている。