SymPy Plotting Backends を使ってみる

SymPy の plot() の残念な点を補う役割をする Sympy Plotting Backends を使ってみると言う話。

Python で関数のグラフを描くということ

授業用資料の準備で SymPy の plot() で初等関数のグラフを描いてみて気づいたこと。

Python で関数のグラフを描く場合,SymPy の plot() を使うと,gnuplot や(gnuplot を使っている)Maxima のように,簡単にグラフを描ける。

matplotlib.pyplot.plot() でもできると言われるかもしれないが,matplotlib.pyplot.plot() は基本的に数値データのリストをプロットするのであって,「関数」を与えてグラフを描くという gnuplot 的,Maxima 的な感じとは少し異なる。

SymPy の plot() の残念なところ

しかしながら,gnuplot や Maxima に比べて,SymPy の plot() は以下のような点で少し残念な仕様になっている。

  1. 例えば $y = \tan x$ の $y \rightarrow – \infty$ と $y \rightarrow + \infty$ は不連続なのに無造作に縦の直線でつないでしまう。
  2. 既定では $x$ 軸と $y$ 軸を描くだけで,グラフの周りに枠を描かないし,グリッドも描けない。(Mathematica の Plot[] のような,といえばわかってもらえるかなぁ。)別途 seaborn 等を使う必要あり。
  3. 数値データのリストをグラフにできない。したがって,実験等で得られた数値データのグラフと関数で表される理論曲線のグラフを重ねて表示することができない。

Sympy Plotting Backends

SymPy の plot() の上記のような残念な点を補う役割をするものとして Sympy Plotting Backends が使えそうなので,少し試ししてみる。

弘大 JupyterHub では

sudo pip install sympy_plot_backends[all]

済み。自身の環境でインストールされていない場合は,各自 pip install する。

gnuplot での描画例の確認

弘大 JupyterHub では,gnuplot_kernel がインストール済みなので,Python 3 のノートブックから gnuplot_kernel を起動できる。gnuplot での描画例と比較して SymPy の plot() の残念なところを確認する。

In [1]:
# Python 3 kernel 内で gnuplot を使うためのおまじない
%load_ext gnuplot_kernel

不連続な関数の plot 例

gnuplot は,以下のように(set sample で大きめの値を設定しておけば)不連続な点を結んでしまうことはしないし,関数の分母がゼロになるような場合でも,特に文句を言わない。

In [2]:
%%gnuplot
set sample 1000
set zeroaxis
set grid

plot [-5:5][-10:10] 1/(x-1) lw 2

In [3]:
%%gnuplot
set zeroaxis
set grid
set sample 1000

plot [-2*pi:2*pi][-10:10] tan(x) lw 2

数値データと関数の plot

gnuplot は,数値データファイルを読み込んでグラフにすることもできる。

例として,以下のような内容のファイル mydata2.txt の数値データを読み込んでグラフにし,関数 $y = x^2$ のグラフと重ねて表示する例。

In [4]:
! cat mydata2.txt
0  0
1  1
2  4
3  9
4 16
5 25
In [5]:
%%gnuplot
set key top left

plot "mydata2.txt" pt 7 title "ファイルの数値データ", \
     [0:5] x**2 lw 2 title "y=x^2"

gnuplot はまた,以下のように1次元配列の数値データをグラフにすることもできる。

In [6]:
%%gnuplot
set key top left

# 数値データを配列として用意。
array X[6]
array Y[6]
do for [i=1:6]{
    X[i]=i-1
    Y[i]=(i-1)**2
    }

# 数値データ と関数を plot
plot Y using (X[$1]):2 pt 7 title "配列の数値データ", \
     [0:5] x**2 lw 2 title "y=x^2"

SymPy での描画例の確認

In [7]:
from sympy import *
# 1文字変数の Symbol の定義が省略できる
from sympy.abc import *
# π
from sympy import pi

# 以下はグラフを SVG で Notebook にインライン表示させる設定
%config InlineBackend.figure_formats = ['svg']

不連続な関数の plot 例

不連続なところを縦の直線でむすんでしまっているなぁ。

In [8]:
plot(1/(x-1), (x, -5, 5), 
     xlim = (-5, 5), ylim = (-10, 10));

不連続な点の近くで曲線がぎこちないので,滑らかにするために adaptive = False, nb_of_points = 1000 などと設定してみる。

In [9]:
plot(1/(x-1), (x, -5, 5), 
     xlim = (-5, 5), ylim = (-10, 10), 
     adaptive = False, nb_of_points = 1000);

In [10]:
plot(tan(x), (x, -2*pi, 2*pi), 
     xlim = (-2*pi, 2*pi), ylim = (-10, 10), 
     adaptive = False, nb_of_points = 1000);

Sympy Plotting Backends の import

In [11]:
from spb import *

既定で外枠とグリッドがつく

Sympy Plotting Backends を import すると,既定で外枠とグリッドがつくようになる。が,不連続点を縦の直線でむすぶ不具合はそのまま。

In [12]:
plot(1/(x-1), (x, -5, 5), 
     xlim = (-5, 5), ylim = (-10, 10));

In [13]:
plot(tan(x), (x, -2*pi, 2*pi), 
     xlim = (-2*pi, 2*pi), ylim = (-10, 10));

不連続な関数の plot の改善例

以下のように detect_poles=True のオプションをつけることで,それらしく(不連続な点を不適切に結ぶことなく)描画されるようになる。

In [14]:
plot(1/(x-1), (x, -5, 5), 
     xlim = (-5, 5), ylim = (-10, 10), 
     detect_poles=True);

不連続な点の近傍の途中で曲線が止まっているので,eps に小さい値を設定してみると,以下のようにそれなりにグラフを描いてくれるようだ。

In [15]:
plot(1/(x-1), (x, -5, 5), 
     xlim = (-5, 5), ylim = (-10, 10), 
     detect_poles=True, eps = 0.0001);

In [16]:
plot(tan(x), (x, -2*pi, 2*pi), 
     xlim = (-2*pi, 2*pi), ylim = (-10, 10), 
     detect_poles=True, eps = 0.0001);

数値データを plot

Sympy Plotting Backends を import すると,plot_list() を使って数値データのリストをグラフにすることができる。

まずは,以下のような内容のファイル mydata2.txt の数値データを読み込んでグラフにする例。

In [17]:
! cat mydata2.txt
0  0
1  1
2  4
3  9
4 16
5 25
In [18]:
import numpy as np

data = np.loadtxt('mydata2.txt')

plot_list(data[:,0], data[:,1]);

plot_list() は既定では上図のように点を線でつないで表示する。

線で繋がずに点として描きたい場合は,以下のように is_point = True を指定する。点の色を指定する場合も(線の色の指定と同様)line_color で。ついでに凡例もつけてみる。

In [19]:
plot_list(data[:,0], data[:,1], "ファイルの数値データ", legend=True, 
          is_point = True, line_color = "red");

以下のような数値データのリストをグラフにすることもできる。

In [20]:
# 数値データのリスト作成
X = [i for i in range(6)]
Y = [i**2 for i in X]
print(X)
print(Y)
[0, 1, 2, 3, 4, 5]
[0, 1, 4, 9, 16, 25]
In [21]:
plot_list(X, Y, "リストの数値データ", legend=True, 
          is_point = True, line_color = "blue");

数値データと関数を重ねて表示

In [22]:
# 数値データの plot(非表示)
p1 = plot_list(X, Y, "リストのデータ", legend=True, 
          is_point = True, line_color = "red", show = False)

# 関数 y = x**2 の plot(非表示)
p2 = plot(x**2, (x, 0, 5), line_color = "blue", show = False)

# 2つを重ねて表示
(p1+p2).show()

作成したグラフをファイルとして保存

Sympy Plotting Backends の plot()plot_list() で作成したグラフをファイルとして保存するには,以下のように .save("filename.svg") などとつけるだけ。

In [23]:
plot(sin(x), (x, -2*pi, 2*pi)).save("spb-sin.svg");

In [24]:
plot_list(X, Y, is_point = True).save("spb-point.svg");

複数のグラフを重ねた場合は,以下のようにして保存できる。ただし,保存されたファイルのグラフは,縦横のサイズが単体のグラフのときよりも少し小さくなるようである。

In [25]:
(p1+p2).save("spb16b.svg")

以下のようにすれば,単体のグラフのときと同じ縦横のサイズで,ファイルに保存できる。

In [26]:
p1.extend(p2)
p1.show()
p1.save("spb16c.svg")

もう少しオプション設定ができればいいのになぁ…

Sympy Plotting Backends はデフォルトでは Matplotlib をバックエンドにしているので,matplotlib.pyplot.plot() でできる以下のような詳細設定ができると,さらにいいのではないかと思う。

  1. grid を細い点線に。
  2. $x$ 軸,$y$ 軸の表示。
  3. xticks, yticks の間隔の設定とラベル。

matplotlib.pyplot.plot() での描画例

たとえば, matplotlib.pyplot.plot() では,以下のように

  1. plt.gird() で grid を細い点線にし,
  2. plt.axhline(0)plt.axvline(0) で $x$ 軸,$y$ 軸を表示し,
  3. plt.xticks()plt.yticks() で目盛の間隔(とラベル)の設定

ができる。

In [27]:
# NumPy も使います
import numpy as np
# Matplotlib でグラフを描きます
import matplotlib.pyplot as plt

# グラフを SVG で Notebook にインライン表示させる設定
%config InlineBackend.figure_formats = ['svg']
In [28]:
x = np.linspace(-np.pi, np.pi, 101)
y = np.sin(x)

# x の目盛
plt.xticks(
    ticks = np.linspace(-np.pi, np.pi, 5), 
    labels = ["-$\pi$", "-0.5$\pi$", "0", "0.5$\pi$", "$\pi$"]
)
# y の目盛を 0.5 刻みに
plt.yticks(np.arange(-1., 1.1, 0.5))

# grid を細い点線で
plt.grid(which="major", color="lightgray", ls="--", linewidth=0.5)

# x軸 y軸
plt.axhline(0, color='black', dashes=(5, 5), linewidth=0.6)
plt.axvline(0, color='black', dashes=(5, 5), linewidth=0.6)

plt.plot(x, y);

… と聞いてみたらできることがわかった!

… という要望を sympy-plot-backends の Issues に書いたら作者から返事があって,「できますよ。」ということ。

Everything you asked is already implemented in Matplotlib, there is no need to re-implement it on this module. In fact, this module is meant to be a starting point for plotting: once your symbolic expressions are plotted, you can extract the figure and axes object and apply all the customization you’d like

要は,一旦 p = plot() などとしてプロットしたら,あとは ax = p.ax として ax に対して通常どおりの Matplotlib のオプション設定をしていけばよいということらしい。

各種オプションの設定については,以下の別ページにまとめてみた。