正規化&フィルタなしでデータからピークを抽出する

こんにちは。橋本です。

自分でデータを取るとき、ノイズが乗らないようにフィルタをかけたり、基準を設けて正規化する等、あとの分析がしやすいように工夫します。
しかしながら「データが得られればいーや。俺分析しないし～」や「なんかこのプログラムに突っ込んだら結果が得られるぜ。工夫？知らんな。」で取られたデータを分析しないとならない場面がちょくちょくあります。

たいていの場合、しこしこと実験をやりなおすのですが、それが叶わない場合、フーリエ変換して高周波成分を除いたり、物理的条件から最小二乗法で同定したりしてどうにか分析しなければなりません。

本稿はそんなデータにおいて、基底に落ちていない(正規化していない)＆ノイズまみれのデータをフィルタレスでピーク値を取得するExce VBA(マクロ)を作りましたので共有します。

ちなみに、このようなデータでピーク値を検出します。赤マーカーが得られたピーク値です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571701/faf12ac7-38b9-e1fa-f994-25eb338e2834.png)
このデータなら、フーリエ変換して高周波取り除いて、物理条件から指数関数的減衰を考慮して、単調増加減少でピークを抽出するのが一般的だと思います。

#どうやってピークを抽出するか
例えば下の図のようなデータがあるとしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571701/7caff56b-d940-63d1-5e10-9d9d368ba184.png)
この図のピークは横軸が5と11のところです。8の点は確かに極大点ですがノイズです。すなわち、真のデータは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571701/e055fb9d-0b9c-d747-e13b-97e0af5891a9.png)
このようになっていると考えられます。この違いをどう見分けるかが肝となってきます。

とりあえず、全ての極大値を洗い出し、その中からノイズ成分であろう部分を取り除く方針で参りましょう。

まず、微分係数を求め、極大値を求めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571701/335392a2-88ac-1537-6155-46e485ebe17c.png)
単純に微分係数が+から-に変わるポイントで極大値が現れます。ここから高さのしきい値を設定する事でノイズを除去できます。
具体的な手順は
1.微分係数が負から正に移る1つ目の極小値を求める
2.微分係数が正から負に移る極大値を求める
3.微分係数が負から正に移る2つ目の極小値を求める
4.極大値と、__大きいほう__の極小値の差が設定したしきい値以上ならピーク
ここで「小さいほう」を選んでしまっては負のノイズを多く拾ってしまいます。

ここでしきい値を3とすれば、横軸5のピークを拾う事ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571701/b078ed52-651f-eb65-bf59-227a681ab470.png)

次に、横軸8を除きながら11を得る方法を考えます。
真のデータから、「横軸6と13に極小値、極大値を11にもつ」と考えて、上のアルゴリズムを走らせれば解けそうです。ここで、横軸9を除く方法は、例えば、ある範囲を決めて、その範囲内に極小値2つと、極大値1つがあるかどうかを判定すれば解決できます。
手順は、
1.上の手順で、4.のときピークでは無かった
2.2つの極小値の距離がある範囲以内のとき
3.極小値の__小さいほう__を極小値の片側に採用
3.微分係数が正から負に移る極大値を求める
4.前に求めた極大値と比較して大きい方を極大値に採用
5.微分係数が負から正に移る2つ目の極小値を求める
6.極大値と、大きいほうの極小値の差が設定したしきい値以上ならピーク
となります。

#コード
よって、コードは以下のようになります。
Excel VBAで制作しました。

```vb
Sub peak_pick()
    'データは見出し行つき,xがx系列,yがy系列
    Dim x, y
    x = 2
    y = 4
    
    '判定高さと判定幅を定義
    Dim hight, width
    hight = 0.4
    width = 10
    
    '最大行番号を取得
    Dim MaxRow
    MaxRow = Cells(1, x).End(xlDown).Row
    'y+1列目に後退微分項を入れる
    Cells(1, y + 1) = "微分"
    Dim i
    For i = 3 To MaxRow
        Cells(i, y + 1) = Cells(i, y) - Cells(i - 1, y)
    Next
    
    'ピーク判定メイン
    Dim mp1_x, mp1_y  '微分係数1つ目の - => +
    Dim mp2_x, mp2_y '2つ目の - => +
    Dim pm_y    ' + => -
    Dim flag1 '+ => -を探すときTrue
    Dim flag2 'width内の探査True
    flag1 = True
    flag2 = False
    Dim j
    j = 2
    Cells(j - 1, y + 2) = "ピークx"
    Cells(j - 1, y + 3) = "ピークy"
    mp1_y = Cells(2, y)
    mp1_x = Cells(2, x)
    
    For i = 3 To MaxRow
        If flag1 Then '+ => -を探す
            'はじめ + => - を探すとは限らないので
            If Cells(i, y) < mp1_y Then
                mp1_y = Cells(i, y)
                mp1_x = Cells(i, y)
            End If
            '+ => -を探す
            If Cells(i, y + 1) > 0 And Cells(i + 1, y + 1) < 0 Then
                If flag2 Then '判定幅内のとき,一番高い数値を使う
                    If pm_y < Cells(i, y) Then
                        pm_y = Cells(i, y)
                        pm_x = Cells(i, x)
                        flag1 = False
                        flag2 = False
                    End If
                Else
                    pm_y = Cells(i, y)
                    pm_x = Cells(i, x)
                    flag1 = False
                End If
            End If
        Else '- => + を探す
            If Cells(i, y + 1) < 0 And Cells(i + 1, y + 1) > 0 Then
                mp2_y = Cells(i, y)
                mp2_x = Cells(i, x)
                '高い方が判定高さ以下なら
                If (mp2_y < mp1_y And (pm_y - mp1_y) > hight) Or (mp2_y > mp1_y And (pm_y - mp2_y) > hight) Then
                    Cells(j, y + 2) = pm_x
                    Cells(j, y + 3) = pm_y
                    j = j + 1
                    flag1 = True
                    mp1_x = mp2_x
                    mp1_y = mp2_y
                '判定高さ以上だが判定幅内のとき
                ElseIf width > (mp2_x - mp1_x) Then
                    If mp1_y > mp2_y Then
                        mp1_y = mp2_y 'mp1_xは固定
                    End If
                    flag1 = True
                    flag2 = True
                'それ以外
                Else
                    flag1 = True
                    mp1_x = mp2_x
                    mp1_y = mp2_y
                End If
            End If
        End If
    Next
End Sub
```
ひっどいコードですが、ご了承ください。

データは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/571701/52324974-edb0-6e78-9f18-8333e298efe8.png)
このようなラベル行つきの配列を想定しています。系列のある列番号をそれぞれ選択し、しきい値高さと幅を指定すれば使用できます。

#参考
きれいなデータのやり方の一例です。
https://qiita.com/akinori-ito/items/005df0d6b73ba56bfe6d