Python / 実験データの自動ローレンチアンフィッティング

##1. はじめに
沢山の実験データを一括でフィッティング処理することを目的に書きました。ひとつひとつデータを開いて、フィッティングして、結果のパラメータを記録して、とやっていると面倒くさいので…。ローレンチアンを例に進めますが、ファイルリストの作成→フィッティングという流れ自体は他にも応用が効くんじゃないかと思います。


##2. 簡単にローレンチアンの話（飛ばしてOK）
ローレンチアン（ローレンツ関数）はこんな関数です。
$$ f(x) = A\frac{d^2}{(x - x_0)^2 + d^2}$$
グラフを描くと $x=x_0$ で極値 $A$ を取る山になります。$d$ は山の半値半幅（HWHM）です。なので半値全幅（FWHM）は $2d$ になります。電磁波の共鳴吸収曲線が丁度この形で書けるため、物理・工学の分野でよくでてきます。


##3. サンプルデータ
実際にデータをいじった方がわかりやすいと思うので、サンプルデータを作ります。

```python:dataset.py
import numpy as np

#パラメータの定義
intensity = 50 #強度
HWHM = 5 #半値半幅
a = 10 #データのばらつきの大きさ

#データファイルの作成
for X0 in range (-30, 31, 10):
    filename = 'X0=' + str(X0) + '.dat'
    with open(filename, 'w') as file:
        file.writelines('X' + '\t' + 'Y' +'\n')
        for X in range (-100,101):
            Y = intensity * HWHM**2 / ((X-X0)**2 + HWHM**2) + a * np.random.rand() - 5
            file.writelines(str(X) + '\t' + str(Y) + '\n')

```


実行するとディレクトリ内に X0=-30〜30 まで、10区切りで7つのdatファイルができます。それぞれタブ区切りで (X, Y) 2列の数値データです。一行目にはインデックスとして X, Y （文字列）を書き込んでいます。中身はさっきのローレンツ関数で、Y=$f($X$)$ です。Yには乱数で若干ばらつきを与えています。例として「X0=0.dat」をプロットすると下のような感じです。

![X0=0.png](https://qiita-image-store.s3.amazonaws.com/0/169883/5b7f5b38-d1dd-ff92-3701-5c5124b65e93.png)





これからこの7つのdatファイルを一括でローレンチアンフィッティングして、各結果のグラフと、収束したパラメータを一つにまとめたdatファイルを作成したいと思います。


##4. 一括でローレンチアンフィッティング
以下本題になります。作成した7つのdatファイルを一括でフィッティングして結果だけ貰いましょう。手順が追いやすいようにjupyterを使いたいと思います。ここからの大まかな流れを先に書いておくと、

1. フィッティングするファイルのリストを作成
2. イテレーションでリスト内のファイルを逐一フィッティング
3. グラフを描画、収束したフィッティングパラメーターを別ファイルに書き出す

になります。

###4.1 フィッティングするファイルのリストを作成する


イテレータで処理するために、globモジュールを用いて、これからフィッティングするファイルのリスト`filelist`を作成します。ここではワイルドカード`*`を使ってフォルダ中の`X0=○○.dat`という名前のファイルだけリスト化します。ということで、jupyterでやりますが、

```py:In[1]
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ptick
import scipy.optimize
import glob, re
from functools import cmp_to_key
```

```py:In[2]
filelist = glob.glob('X0=*.dat')
filelist2 = []

for filename in filelist:
    match = re.match('X0=(.*).dat', filename)
    tuple = (match.group(1), filename)
    filelist2.append(tuple)

def cmp(a, b):
    if a == b: return 0
    return -1 if a < b else 1

def cmptuple(a, b):
    return cmp(float(a[0]), float(b[0]))

filelist = [filename[1] for filename in sorted(filelist2, key=cmp_to_key(cmptuple))]
filelist
```

```py:Out[2]
['X0=-30.dat',
 'X0=-20.dat',
 'X0=-10.dat',
 'X0=0.dat',
 'X0=10.dat',
 'X0=20.dat',
 'X0=30.dat']
```

参考：[Python: ファイル名の番号でソートする](http://hiroyky.blogspot.jp/2014/06/python_4.html
)（外部サイト）


を実行します。`In[2]`でやっているのはリストの作成とリスト内のファイルソートです。`glob`で捕まえるだけだと、文字列で並び替えられるため、数字の小さい順に並びません。それで大して困ることもないのですが、後々気持ち悪さも残るので、ここで数字の小さい順に並び替えています。




###4.2 初期パラメータの推定とフィッティング
どうしてもループの中が少し長くなってしまうのですが…、こんな感じです。（後ろで説明します。）

```py:In[3]
result = open('fitting_result.dat', 'w')
result.writelines("filename\tA\tA_err\tX0\tX0_err\tHWHM\tHWHM_err\n")

for filename in filelist:
    print(filename)
    match = re.match('(.*).dat', filename)
    name = match.group(1)

    with open(filename, 'r') as file:
        X, Y = [], []

        for line in file.readlines()[1:]:
            items = line[:-1].split('\t')
            X.append(float(items[0]))
            Y.append(float(items[1]))

    # 初期値の推定（強度と中心値）
    max_Y = max(Y)
    min_Y = min(Y)

    if np.abs(max_Y) >= np.abs(min_Y):
        intensity = max_Y
        X0 = X[Y.index(max_Y)]

    else:
        intensity = min_Y
        X0 = X[Y.index(min_Y)]

    # 初期値の設定
    pini = np.array([intensity, X0, 1])

    # フィッティング
    def func(x, intensity, X0, HWHM):
        return intensity * HWHM**2 / ((x-X0)**2 + HWHM**2)
    
    popt, pcov = scipy.optimize.curve_fit(func, X, Y, p0=pini)
    perr = np.sqrt(np.diag(pcov))

    # 結果の表示
    print("initial parameter\toptimized parameter")
    for i, v  in enumerate(pini):
        print(str(v)+ '\t' + str(popt[i]) + ' ± ' + str(perr[i]))
    
    # datファイルへのフィッティング結果の書き込み
    result.writelines(name + '\t' + '\t'.join([str(p) + '\t' + str(e)  for p, e in zip(popt, perr)]) + '\n')
    
    # フィッティング曲線描画用の配列を作成
    fitline = func(X, popt[0], popt[1], popt[2])

    # 結果のプロットと画像の保存
    plt.clf()
    plt.figure(figsize=(10,6))
    plt.plot(X, Y, 'o', alpha=0.5)
    plt.plot(X, fitline, 'r-', linewidth=2)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.ticklabel_format(style='sci',axis='y',scilimits=(0,0))
    plt.savefig(name + ".png")
    plt.close()
    
result.close()
```
```text:In[3]の結果
X0=-30.dat
initial parameter	optimized parameter
52.8746388447	52.1363835425 ± 1.37692592137
-29.0	-30.2263312447 ± 0.132482308868
1.0	5.01691953143 ± 0.187432386079
・・・
（中略）
・・・
X0=30.dat
initial parameter	optimized parameter
50.0825336071	50.5713312616 ± 1.54634448135
31.0	30.1170389209 ± 0.149532401478
1.0	4.89078858649 ± 0.211548017933
```

ひとつひとつ説明したいと思います。まず、コメントアウトした`# 初期値の推定`より上ですが、始めに最後の結果を書き出すファイルの用意（ファイルの作成とインデックスの書き込み）をしています。そして、for文以下でデータファイルのリストを使ってイテレーションを開始しています。

```py:
match = re.match('(.*).dat', filename)
name = match.group(1)
```
のところは、最後に画像を書き出す時のファイル名用に`.dat`の拡張子前の文字列を`name`で抜き出しています。

次は`# 初期値の推定`のところです。今回のフィッティングで**初期パラメータ**（`pini`）として必要なのは、**ピーク強度**（`intensity`）、**ピークを取るときのX**(`X0`)、**半値半幅**（`HWHM`）です。どのくらいの精度で推定しておくかというのが難しいんですが、ここではそれぞれ以下のように推定しています。

推定していく順番に、
`intensity`：Yの最大値・最小値を取り出し、絶対値が大きい方を採用
`X0`：採用した`intensity`が出るときのXの値を採用
`HWHM`：推定してません。とりあえず１にしています。上2つのパラメータはだいぶ正確なのでここは妥協して良さそうです。


そして、`#　フィッティング`のところ。推定した初期パラメータを用いてフィッティングします。ここはSciPyのフィッティング手順に沿ってフィッティングをしています。収束したパラメーターは`popt`に格納されます。`perr`は標準誤差です。フィッティングパラメーターの共分散`pcov`の対角成分のルートを取っています。

参考：
・[Pythonで非線形関数モデリング](http://qiita.com/hik0107/items/9bdc236600635a0e61e8)
・[scipy.optimize.curve_fit（SciPyの公式ドキュメント）](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html#scipy.optimize.curve_fit)

最後は`# 結果の表示`と`# datファイルへのフィッティング結果の書き込み`、`# フィッティング曲線描画用の配列を作成`、`# 結果のプロットと画像の保存`です。`# 結果の表示`と`# datファイルへのフィッティング結果の書き込み`はそのままなので省略します。`# フィッティング曲線描画用の配列を作成`は、収束したパラメーターを使って定義した関数にデータのXを代入した結果です。なのでここではフィッティングラインのデータを元データと同じ点数で用意しています。`# 結果のプロットと画像の保存`もそのままなので省略します。

```py:
plt.savefig(name + ".png")
```
一応このようにして最初の`re.match`の結果である`name`を使っています。


##5. 結果
フィッティング結果の画像の方はこんな感じです。

![X0=0.png](https://qiita-image-store.s3.amazonaws.com/0/169883/a77d7509-95ac-09d2-a929-73285a34196e.png)


そして、数値としての結果（収束したフィッティングパラメータと標準誤差をタブ区切りでまとめたもの）は`fitresult.dat`に書き出されます。


##6. おわりに
ということで、めでたく7つのファイルを一括でフィッティングすることができました。今回は簡単のため7つでしたが、まあ実験をやっていると数十から場合によっては百を超えるようなデータをフィッティングしないといけない時もあるので、そういう時には力を発揮すると思います。




##7. 補足
このくらいピークが明瞭なら、初期パラメータなんて全部適当に固定してしまってもうまくやってくれるんじゃないの？って思う人もいると思います。ただ、初期パラメータにオーダーレベルでずれがあると、

![X0=30.png](https://qiita-image-store.s3.amazonaws.com/0/169883/af06ffd3-3891-be34-1b14-8f04a3c5f9c1.png)

みたいなことになるのが実情です。線形フィッティングとかだと初期値の推定は必要ないと思いますが、やや関数が複雑になれば、ある程度（オーダーを見積もる程度でいいので）初期値を推定させた方が良さそうです。


Jupyterに馴染みがない方もいるかもしれないので、最後に`.py`の形にしたものも置いておきます。

```py:lorentzian_fittng.py
import numpy as np
import matplotlib.pyplot as plt
import scipy.optimize
import glob, re
from functools import cmp_to_key


filelist = glob.glob('X0=*.dat')
filelist2 = []


'''
/////////////////////////////////
データファイルのソート
/////////////////////////////////
'''
for filename in filelist:
    match = re.match('X0=(.*).dat', filename)
    tuple = (match.group(1), filename)
    filelist2.append(tuple)

def cmp(a, b):
    if a == b: return 0
    return -1 if a < b else 1

def cmptuple(a, b):
    return cmp(float(a[0]), float(b[0]))

filelist = [filename[1] for filename in sorted(filelist2, key=cmp_to_key(cmptuple))]


'''
/////////////////////////////////
解析（ローレンチアンフィッティング）
/////////////////////////////////
'''
result = open('fitting_result.dat', 'w')
result.writelines("filename\tA\tA_err\tX0\tX0_err\tHWHM\tHWHM_err\n")

for filename in filelist:
    print(filename)
    match = re.match('(.*).dat', filename)
    name = match.group(1)

    with open(filename, 'r') as file:
        X, Y = [], []

        for line in file.readlines()[1:]:
            items = line[:-1].split('\t')
            X.append(float(items[0]))
            Y.append(float(items[1]))

    # 初期値の推定（強度と中心値）
    max_Y = max(Y)
    min_Y = min(Y)

    if np.abs(max_Y) >= np.abs(min_Y):
        intensity = max_Y
        X0 = X[Y.index(max_Y)]

    else:
        intensity = min_Y
        X0 = X[Y.index(min_Y)]

    # 初期値の設定
    pini = np.array([intensity, X0, 1])

    # フィッティング
    def func(x, intensity, X0, HWHM):
        return intensity * HWHM**2 / ((x-X0)**2 + HWHM**2)

    popt, pcov = scipy.optimize.curve_fit(func, X, Y, p0=pini)
    perr = np.sqrt(np.diag(pcov))

    # 結果の表示
    print("initial parameter\toptimized parameter")
    for i, v  in enumerate(pini):
        print(str(v)+ '\t' + str(popt[i]) + ' ± ' + str(perr[i]))

    # datファイルへのフィッティング結果の書き込み
    result.writelines(name + '\t' + '\t'.join([str(p) + '\t' + str(e)  for p, e in zip(popt, perr)]) + '\n')

    # フィッティング曲線描画用の配列を作成
    fitline = []
    for v in X:
        fitline.append(func(v, popt[0], popt[1], popt[2]))

    # 結果のプロットと画像の保存
    plt.clf()
    plt.figure(figsize=(10,6))
    plt.plot(X, Y, 'o', alpha=0.5)
    plt.plot(X, fitline, 'r-', linewidth=2)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.ticklabel_format(style='sci',axis='y',scilimits=(0,0))
    plt.savefig(name + ".png")
    plt.close()

result.close()

```