Scipy

wikiによれば

SciPyはPythonのための科学的ツールのオープンソース・ライブラリとして開発されている。 SciPyは配列の高速な操作のためのすべてのライブラリを含んでおり、 人気のNumericモジュールを置き換え、 ひとつのパッケージとして高レベルな科学と工学のモジュールを集めたもの。

SciPyは、配列オブジェクトとその他の基本的な機能を備えたNumPyを基礎にしている。 SciPy は統計、最適化、積分、線形代数、フーリエ変換、 信号・イメージ処理、遺伝的アルゴリズム、ODE (常微分方程式) ソルバ、特殊関数、その他のモジュールを提供する。 Cythonで記述することがある。

SciPyもpandasと同様にできることが多い。 特殊関数や方程式の数値解法、フーリエ変換、近似曲線、線形代数、微積などが行えるが、 ここでは主に実験データ解析に必要であろう物理定数とデータ補間について説明する。

物理定数

SciPyはconstantsパッケージ内に物理定数がいろいろ入っている。

[参照] https://docs.scipy.org/doc/scipy/reference/constants.html


from scipy import constants

print('光速', constants.c)
print('プランク定数', constants.h)
print('電気素量', constants.e)
print('ボルツマン定数', constants.k)
print('アボガドロ定数', constants.N_A)
_c_ = (constants.mu_0 * constants.epsilon_0)**(-1/2)
print('1/c^2 = mu_0 x epsilon_0', _c_)

定数 変数名 単位
光速(真空中) c, speed_of_light 299792458.0 m s-1
磁気定数(真空の透磁率) mu_0 1.25663706212e-06 N A-2
電気定数(真空の誘電率) epsilon_0 8.8541878128e-12 F m-1
プランク定数 h, Planck 6.62607015e-34 J s
換算プランク定数 hbar 1.0545718176461565e-34 J s
重力定数 G, gravitational_constant 6.6743e-11 m3 kg-1 s-2
標準重力加速度 g 9.80665 m s-2
電気素量(素電荷、電荷素量) e, elementary_charge 1.602176634e-19 C
気体定数 R, gas_constant 8.314462618 J K-1 mol-1
微細構造定数 alpha, fine_structure 0.0072973525693 無次元
アボガドロ定数 N_A, Avogadro 6.02214076e+23 mol-1
ボルツマン定数 k, Boltzmann 1.380649e-23 J K-1
シュテファン=ボルツマン定数 sigma, Stefan_Boltzmann 5.670374419e-08 W m-2 K-4
ウィーンの変位定数 Wien 0.002897771955 m K
リュードベリ定数 Rydberg 10973731.56816 m-1
電子質量 m_e, electron_mass 9.1093837015e-31 kg
陽子質量 m_p, proton_mass 1.67262192369e-27 kg
中性子質量 m_n, neutron_mass 1.67492749804e-27 kg

上記表の単位は全てSI(MKSA)単位系である(CGS系で使いたいなら単位変換が必要)。

データの補間

サンプルデータ。

測定した実験データの点は離散的であるから 本来はデータ点の間の領域の情報を得ることはできるない。 しかし、領域内に適当な曲線を当てはめることで 測定されたデータ点以外の情報も推定することができるようになる。

例として次のようなデータが測定されたとする。

x = [1, 2, 3, 5], y = [2, 4, 1, 3]

これをプロットしたのが右図である。 測定データとしてマーカーがプロットされた点は測定データであるから \(x=2\to y=4\)などと情報が得られる。 しかし、それ以外の点、例えば\(x=1.5\)や\(x=4\)のときの\(y\)の値は不明である。 これらの値を求めるときに行うのが「補間」あるいは「内挿」である。

scipyで補間を行うには"scipy.interpolate"モジュールを用いる。 1変数\(y=f(x)\)での補間であれば、"interp1d"を用いることで補完関数を取得できる。

from scipy import interpolate
import numpy as np
import matplotlib.pyplot as plt

# 観測データ
observed_x = [1, 2, 3, 5]
observed_y = [2, 4, 1, 3]

# 補間関数取得
f = interpolate.interp1d(observed_x, observed_y)

# 連続した新しいxデータ
new_x = np.linspace(min(observed_x), max(observed_x), 100)
# 補間関数にnew_xを代入し、それに対応するyを取得(new_yに代入)
new_y = f(new_x)

print('x=1.5:', f(1.5))
print('x=4.0:', f(4.0))

# プロット
plt.scatter(observed_x, observed_y)     # 散布図(観測データ)
plt.plot(new_x, new_y)                  # 補間曲線
plt.xlabel(r'$x$')
plt.ylabel(r'$y$')
plt.grid()
plt.show()

上記コードで重要なのは10行目であり、 ここで観測されたx,yデータに基づいた補間関数を生成している。 渡せる主なパラメータは以下の通りである。

from scipy import interpolate
f = interpolate.interp1d(
    x,
    y,
    kind = 'linear',
    fill_value = nan
    )

"kind"では補間の種類を指定できる。 デフォルトでは'liner'が指定されている。 "fill_value"では外挿値を指定できる。 デフォルトではnanが指定されており、外挿しようとするとエラーになる。 (上記コードだとxの範囲が1~5までであり、f(1.5),f(4.0)は内挿に相当する為、 エラーはでないが、f(0)やf(6)はエラーとなる。) 外挿を行いたい場合は、"fill_value"に"extrapolate"を指定することで行える。

from scipy import interpolate
import numpy as np

# 観測データ
observed_x = [1, 2, 3, 5]
observed_y = [2, 4, 1, 3]

# 補間関数取得
f = interpolate.interp1d(
    observed_x,
    observed_y,
    kind = 'linear',                # 補間手法をlinearと明示
    fill_value = 'extrapolate'      # 外挿も行えるように指定
    )

print('内挿 x=1.5:', f(1.5))
print('内挿 x=4.0:', f(4.0))
print('外挿 x=0.0:', f(4.0))
print('外挿 x=6.0:', f(6.0))

補間手法

補間手法として、上記例ではデフォルトの'linear'(線形補完)のコードを示した。 scipyでは他にも補間手法が容易されており、それらは以下の通りである。
引数名 補間方法
linear 線形補間(デフォルト)
nearest 最近傍補間(切捨)
nearest-up 最近傍補間(切上)
zero 0次スプライン
slinear 1次スプライン
quadratic 2次スプライン
cubic 3次スプライン
previous 前方参照
next 後方参照
各補間法での振舞いを確認するコードを下記に記す。

import matplotlib.pyplot as plt
import numpy as np
from scipy import interpolate

# 観測データ
x = np.array([0.2, 0.9, 2.2, 2.9, 3.8, 5.1, 6.2])
y = np.array([4.6, 12.5, 6.7, 9.0, 3.4, 5.8, 4.5])

# 補間方法一覧とプロット時のラベルと色指定
KIND_DICT = {
    'linear':
        {'label': '線形補間', 'color':'orange'},
    'nearest':
        {'label':'最近傍補間(切捨)', 'color':'blue'},
    'nearest-up':
        {'label':'最近傍補間(切上)', 'color':'cyan'},
    'zero':
        {'label':'0次スプライン', 'color':'green'},
    'slinear':
        {'label':'1次スプライン', 'color':'red'},
    'quadratic':
        {'label':'2次スプライン', 'color':'purple'},
    'cubic':
        {'label':'3次スプライン', 'color':'pink'},
    'previous':
        {'label':'前方参照', 'color':'brown'},
    'next':
        {'label':'後方参照', 'color':'olive'}
    }

# 連続xデータ
new_cont_x = np.linspace(x.min(), x.max(), 100)

# figure作成
fig = plt.figure()
cnt = 1
# KIND_DICT辞書をforで回す
for key in KIND_DICT:
    # 補間関数作成
    f = interpolate.interp1d(x, y, kind=key, fill_value='extrapolate')
    # ax作成
    ax = fig.add_subplot(3, 3, cnt)
    # 観測データプロット
    ax.scatter(x, y, marker='o', c='k')
    
    color = KIND_DICT[key]['color']     # 補間法ごとの色
    label = KIND_DICT[key]['label']     # 補間法ごとのラベル
    # 補間曲線プロット
    ax.plot(new_cont_x , f(new_cont_x), ls='--', c=color, label=label)
    ax.legend()     # 凡例表示
    ax.grid()       # グリッド表示
    cnt += 1

# 図の余白微調整
fig.tight_layout()
fig.subplots_adjust(wspace=0.15, hspace=0.15)
plt.show()

補間法による振舞いの違い

スプライン補間

前任者に作っていただいた実験データ解析プログラムでは補間法として"quadratic"を採用している。 ここではスプライン補間について紹介する。

スプライン補間とはデータを区分ごとに分割し、その区分領域を多項式で表現する方法である。 2点(\(x_{i}, x_{i+1}\))間の補間関数を\(S_i(x)\)と表せば、全体の補間関数\(S(x)\)は \begin{align*} S(x)=\begin{cases} S_0(x)&(x_0\leq x < x_1)\\ S_1(x)&(x_1\leq x < x_2)\\ \vdots\\ S_{N-1}(x)&(x_{N-1}\leq x < x_N) \end{cases} \end{align*} と書ける。ここで\(N\)はデータ点の個数である。

スプライン補間。図は"quadratic"を採用したもの。

\(S_i(x)\)として3次関数\(f(x)=ax^3+bx^2+cx+d\)を採用したものを「3次スプライン補間」と呼び、 2次関数を採用したものを「2次スプライン補間」、1次関数を採用したものを「1次スプライン補間」、 0次関数を採用したものを「0次スプライン補間」と呼ぶ。

3次スプライン補間

3次スプライン補間の場合、関数を決定するには係数\(a,b,c,d\)を求める必要があるが、 スプライン補間では条件として以下を課すという特徴をもつ。

  1. \(S_i(x)\)はデータ点を通る。 即ち、\(S_i(x_i)=y_i,S_i(x_{i+1})=y_{i+1}\)という条件を満たす。
  2. データ点にて隣り合う区間の1階微分係数が一致する。 即ち、\({S_i}'(x_i)={S_{i+1}}'(x_i)\)という条件を満たす。
  3. データ点にて隣り合う区間の2階微分係数が一致する。 即ち、\({S_i}''(x_i)={S_{i+1}}''(x_i)\)という条件を満たす。
なお、これだけだと係数を決定するには条件が2つ足りない為、 データ両端(\(x_0,x_N\))の2階微分係数が0という条件 (\({S_0}''(x_0)={S_{N-1}}''(x_N)=0\))を加える。

3次スプライン補間がよく使われるらしく、これに関してはネットでもテキストが見つかる。 前任者のコードでは"quadratic"(2次スプライン補間)が使われているので これにもう少し詳しく見ていく

2次スプライン補間

2次スプライン補間の条件は以下である。

  1. \(S_i(x)\)はデータ点を通る。 即ち、\(S_i(x_i)=y_i,S_i(x_{i+1})=y_{i+1}\)という条件を満たす。
  2. データ点にて隣り合う区間の1階微分係数が一致する。 即ち、\({S_i}'(x_i)={S_{i+1}}'(x_i)\)という条件を満たす。
2次スプライン補間の場合、(\(N-1\))個の\(S_i(x)=a_ix^2+b_ix+c_i\)に関して、 \(a,b,c\)の3種類の係数を求める必要がある為、計\(3(N-1)\)個の条件式が必要となる。

条件(1)の\(S_i(x_i)=y_i\)より\(N\)個の条件式と \(S_i(x_{i+1})=y_{i+1}\)より\(N-2\)個の条件式が出てくる。 また、条件(2)の\({S_i}'(x_i)={S_{i+1}}'(x_i)\)より\(N-2\)個の条件式が出てくる。 よって、\(N+N-2+N-2=(3N-4)\)個の条件式を得る。 必要な条件式の個数は\(3(N-1)\)個であったから1つ足りないことになる。

※さて、分からないのがここのあと1つの条件である。 ネットの海を漂い、2次スプライン補間について書かれたものを見ると、 最後の条件として左端のデータ点で2階微分係数が0というものを見つけた。 [https://engcourses-uofa.ca/books/numericalanalysis/piecewise-interpolation/quadratic-spline-interpolation/] (3次スプラインについては大量の文献が見つかるのに、 2次スプラインについて書かれたものがほとんどない。) つまり、\(a_0=0\)であるから\(S_0(x)=b_0x+c_0\)となり、 一番左の領域は直線で描かれることになる。 うーん。Scipyだとなっていないですね。 何かしらもう1つ条件式が欲しいのだが、それが分からない。 Scipyの元のコードをみれば分かるのだろうか。 おそらくFortranで書かれてそうなので読みたくない。 というより、"quadratic"のグラフを見るとどうも2次関数には見えない領域がある。 両端だけ2次関数でそれ以外は3次関数で補間してるとかありそう…。

0次・1次スプライン補間

1次スプライン補間は\(S_i(x)=a_ix+b_i\)の直線での補間であるから、これは線形補間に等しい。 また、0次スプライン補間は定数値での補間となりScipyでは前方参照と同じ結果となる。

特殊関数

scipy.specialパッケージ内に特殊関数がいろいろ入っている。

[参照] https://docs.scipy.org/doc/scipy/reference/special.html

詳しくは上記の公式ページを参照。 ここではガンマ関数を紹介して終わる。 ガンマ関数は階乗を拡張した表現で \begin{align} \Gamma(x)=\int_{0}^{\infty}t^{x-1}e^{-t}dt \end{align} で書ける。


from scipy.special import gamma
import numpy as np
import matplotlib.pyplot as plt

def factorial(n):
    if n == 0:
        return 1
    relust = n
    for x in range(n-1, 0, -1):
        relust *= x
    return relust

print(gamma(4))
print(gamma(1/2), np.pi**(1/2))  # piの平方根と等しい

x = np.linspace(0, 10, 100)
n = np.arange(0, 10)

np_factorial = np.frompyfunc(factorial, 1, 1)
y1 = np_factorial(n)
y2 = gamma(x+1)

fig, ax = plt.subplots()
ax.plot(x, y2, c='b')
ax.scatter(n, y1, c='r')
ax.set_yscale('log')
plt.show()

上記コードでは試したないがgamma()は負の領域でも使えるので グラフ化してみると面白しろいかもしれない。

微分・積分

数値微分及び数値積分ができるので紹介しておく。


from scipy.misc import derivative
import numpy as np
import matplotlib.pyplot as plt

def func1(x):
    return (x+3) * (x-1) * (x-4)

x = np.linspace(-4, 5, 100)

# x=3における1階微分係数
_X_ = 3
a1 = derivative(func1, _X_, dx=1e-5, n=1)

# y切片計算
a0 = func1(_X_) - a1 * _X_
# 接線
y = a1*x + a0

fig, ax = plt.subplots()

ax.plot(x, func1(x), c='b')
ax.plot(x, y, c='r')

ax.set_xlim(-4, 5)

plt.show()

次に定積分の紹介上記のfunc1と同じ関数を-3から3の範囲で積分している。


import numpy as np
from scipy import integrate
import matplotlib.pyplot as plt

def func1(x):
    return (x+3) * (x-1) * (x-4)

x = np.linspace(-4, 5, 100)

print(integrate.quad(func1, -3, 3))

fig, ax = plt.subplots()
ax.plot(x, func1(x), c='b')
x_ = np.linspace(-3, 3, 100)
ax.fill_between(x_, func1(x_), 0, facecolor='lime', alpha=0.5)
ax.set_xlim(-4, 5)
ax.grid()
plt.show()

積分範囲に無限大(np.inf)の指定もできる。 折角なのでガンマ関数を実装してみる。


import numpy as np
from scipy import integrate
from scipy.special import gamma
import matplotlib.pyplot as plt

def func2(t, x):
    return t**(x-1) * np.exp(-t)

# Gamma(5) = 4! の計算
print(integrate.quad(func2, 0, np.inf, args=5))     # 下限:0、上限:無限の積分 
print(integrate.quad(func2, 0, 1e+4, args=5))       # 下限:0、上限:10^4の積分
# Gamma(1/2)の計算
print(integrate.quad(func2, 0, np.inf, args=1/2))
print(integrate.quad(func2, 0, 1e+3, args=1/2))     # 下限:0、上限:10^3の積分
print('root pi = ', np.pi**(1/2))

def myGamma_upinf(x):
    return integrate.quad(func2, 0, np.inf, args=x)[0]

def myGamma_upnum(x):
    return integrate.quad(func2, 0, 1e+4, args=x)[0]

# ユニバーサル関数に変換
np_myGamma1 = np.frompyfunc(myGamma_upinf, 1, 1)
np_myGamma2 = np.frompyfunc(myGamma_upnum, 1, 1)
x = np.linspace(1, 10, 1000)

fig = plt.figure()
ax1 = fig.add_subplot(121)
ax1.set_title('Gamma Function')
ax1.plot(x, gamma(x), c='b', label='scipy')
ax1.plot(x, np_myGamma1(x), ls='--', c='r', label='upper limit oo')
ax1.plot(x, np_myGamma2(x), ls='--', c='g', label='upper limit 1e+4')
ax1.set_yscale('log')
ax1.legend()

ax2 = fig.add_subplot(122)
ax2.set_title(r'My $\Gamma$ / SciPy $\Gamma$')
ax2.plot(x, np_myGamma1(x)/gamma(x), c='r', label='upper limit oo')
ax2.plot(x, np_myGamma2(x)/gamma(x), c='g', label='upper limit 1e+4')
ax2.legend()
plt.show()

方程式の数値解

用いるのはoptimize.fsolve(func, x0)


import numpy as np
from scipy.optimize import fsolve
import matplotlib.pyplot as plt

def f(x):
    return x

def g(x):
    return np.cos(x)

def h(x):
    return f(x) - g(x)

p = fsolve(h, 0)    #0近傍から解を探す
print('交点p', p)
print('f(p)', f(p))
print('g(p)', g(p))

x = np.linspace(0, 3, 50)
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.plot(x, f(x))
ax1.plot(x, g(x))

ax1.scatter(p, f(p), color='g')

plt.show()

交点が複数個ある場合はfsolveの引数x0にリストで複数の初期値を渡すと上手くいく。


import numpy as np
from scipy.optimize import fsolve
import matplotlib.pyplot as plt

def f(x):
    return x + 2

def g(x):
    return x**2

def h(x):
    return f(x) - g(x)

p = fsolve(h, [0, 1])    #0と1近傍から解を探す(2個の解が出る)
print('交点p', p)
print('f(p)', f(p))
print('g(p)', g(p))

x = np.linspace(-2, 3, 50)
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.plot(x, f(x))
ax1.plot(x, g(x))

ax1.scatter(p, f(p), color='g')

plt.show()