SPSS ModelerのCLEM関数@OFFSET, @DIFF, @MEANをPythonで書き換える。 時系列に記録されたセンサーデータの加工

SPSS Modelerで時系列データを分析する際、フィールド作成ノードでよく利用するCLEM関数の<span>@<span>OFFSET
、<span>@<span>DIFF、<span>@<span>MEAN をPythonのpandasで書き換えてみます。

<span>@<span>OFFSETという関数は、現在の行を起点に、前後の行を参照するという関数です。
時系列データにおいて、過去や未来の実績値をもとに新しい列を作成するために用いることが多いです。

<span>@<span>DIFF1という関数は、1行前との差分を計算します。
時系列データにおいて、時系列的な変化を把握するために用いることが多いです。

<span>@<span>MEANという関数は、移動平均（=時系列データのある範囲の平均）を計算するために使用します。
具体的には、現在の行を含むN行前までのデータの平均を計算します。
時系列データにおいて、細かい値の変動を取り除き、全体的なトレンドを把握するために用いることが多いです。

下記記事で取り扱っている故障予知のシナリオでは、CLEM関数の<span>@<span>OFFSET、<span>@<span>DIFF1、<span>@<span>MEANを組み合わせてデータ加工を行っています。
[SPSS Modelerで時系列データをつかった故障分析②故障予知](https://qiita.com/kawada2017/items/a7fa843e166dfa98ec73)
著者に許可をいただきましたので、記事の内容をもとに、CLEM関数の<span>@<span>OFFSETと<span>@<span>DIFF関数を、Pythonのpandasへ書き換えます。


#1. 加工のイメージ
センサーデバイスから取得した時系列データに対して、①と② の加工を行います。
①5時間後のエラーコード(ERR_CD)を持つ列の作成
②電力(POWER)および温度(TEMP)について1時間前の測定値からの差分を持つ列の作成 

- センサーから取得した時系列データに含まれる項目
    - M_CD: マシンコード
    - UP_TIIME: 起動時間
    - POWER: 電力
    - TEMP: 温度
    - ERR_CD: エラーコード

■加工前
M_CDとERR_CDは左寄せの文字型で表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/dae8ef45-f9e4-76a6-f55d-ffb518156b5a.png)


■加工後
5時間後のエラーコードを持つ列(ERR_CD_5FUTURE), 1時間前の電力を持つ列(POWER_DIFF), 1時間前の温度を持つ列(TEMP_DIFF) が追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/b0d56a6e-acd6-3c48-9d5d-b2da0d4da6ff.png)


#2. Modelerフィールド作成ノードでの設定 
## ① 5時間後のエラーコード(ERR_CD)を持つ列の作成

下記の記事より、「目的変数（ラベル）の作成」の節から該当部分を引用の上、前後の説明のために内容を若干改変しています。
詳細は記事をご覧ください。

[SPSS Modelerで時系列データをつかった故障分析②故障予知](https://qiita.com/kawada2017/items/a7fa843e166dfa98ec73)

**目的変数（ラベル）の作成**

故障を予知するということは、モデルは実際の発生よりも前にエラーが発生することを予測しなければ意味がありません。

事前に予測ができれば、点検や部品交換などをおこなってエラーを回避することができれるかもしれません。では何時間前に予測できれば良いのでしょうか？
これはデータを見るだけではわかりません。業務担当者と話をして何時間前に予測ができれば対処が可能かを話して決めることになります。

ここでは5時間前にわかればよいと想定して分析を進めます。

5時間前にエラーが発生することを予測するためにはどうすればよいでしょうか？
それには5時間後のエラーコードを目的変数にするという方法があります。この変数は元データにはないので新たに生成します。
フィールド作成ノードを接続し、ダブルクリックしてプロパティを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/f436975a-0d4e-e4a9-5b69-9e9964f9b167.png)

「派生フィールド」名に「ERR_CD_5FUTURE」という名前を指定します。
そして「派生」で「条件付き」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/2b922ad2-6e6b-bacf-a48c-59df4476291c.png)

そして
IFに「M_CD = <span>@<span>OFFSET(M_CD,-5)」
Then:に「<span>@<span>OFFSET(ERR_CD,-5)」
Else:に「undef」
を設定します。
<span>@<span>OFFSETという関数は現在処理している行の前や後の行を参照するという関数です。
M_CD が5行先と同じ場合に5行先のERR_CDを設定する、M_CD が5行先と違う場合はNULL（undef)を設定するという意味です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/e4bd286f-4779-d5e8-9a18-6d58b831131f.png)

OKで閉じてください。

少し難しいのでデータを見てみましょう。

101のエラーの発生しているM_CD=104のデータを見てみましょう。
条件抽出ノードを接続し、M_CD='104'を入力し、OKで閉じてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/96a7b016-ebf7-5fe6-b030-49f56d5c0c12.png)
「テーブル」ノードを接続し実行してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/16c1dde0-35a6-b108-886c-6703c495b45a.png)

43時間目に101のエラーが発生していますが、新しく生成したERR_CD_5FUTUREには38時間目から101のデータが入っています。@OFFSETを使うことで5行後ろのデータを持ってきたのです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/e9dcc257-5597-9ed3-a0ce-c588a8a82299.png)

次に一番下までスクロールしてください。ERR_CD_5FUTUREの最後の5行はNULLになっています。これは5時間先のエラーコードのデータがそもそも存在しないということです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/3544523e-285e-be2b-1661-a5ef6e6ac23b.png)

## ②電力(POWER)および温度(TEMP)について1時間前の測定値からの差分を持つ列の作成

下記の記事より、「特徴量抽出」の節から該当部分を引用の上、前後の説明のために内容を若干改変しています。
詳細は記事をご覧ください。

[SPSS Modelerで時系列データをつかった故障分析②故障予知](https://qiita.com/kawada2017/items/a7fa843e166dfa98ec73)

**特徴量抽出**

時系列データはその変化を新しい特徴量として加工することでより説明力を高めることができます。
データの差（変化）をとったり、移動平均をとってならすことで多少のブレを吸収したりすることができます。

まず差分をとってみます。
ソートノードとERR_CD_5FUTUREのフィールド作成ノードの間を広げて、新しいフィールド作成ノードをつなぎます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/c1f906f4-e9e6-6bb6-7348-04edf15ad6c8.png)

ダブルクリックしてプロパティを開きます。
モードを「複数」に設定します。そして、利用可能なフィールドの設定から取得ボタンを押して、POWERとTEMPを選びます。フィールドリストに表示されます。

フィールド名拡張子に「_DIFF」を設定します。
そして派生で「条件付き」を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/63cd3cc8-66e9-0ef3-7b17-200adff0fc45.png)


以下を設定します。
If:M_CD = <span>@<span>OFFSET(M_CD,1)
Then:<span>@<span>DIFF1(<span>@<span>FIELD)
Else:undef

先ほどERR_CD_5FUTUREを生成した時の応用になります。
<span>@<span>OFFSETという関数は今いる行の前や後の行を参照するという関数でした。
<span>@<span>DIFFは1行前との差分を計算します
<span>@<span>FIELDはフィールドリストにあげた複数の変数に対して同じ処理を行うという意味です。
M_CD が1行前と同じ場合にPOWERとTEMPについて1行前との差分を設定し、M_CD が1行前と違う場合はNULL（undef)を設定するという意味です。
設定ができたらOKで閉じてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/2eff9580-ff12-5983-d854-f8c681652e12.png)

テーブルノードを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/014f3d7b-9e62-6445-4343-a0aa251fb997.png)

POWER_DIFFとTEMP_DIFFという列が生成されています。
1時間目のPOWER_DIFFは1時間目の941ワットから0時間目の945ワットを引いた-4ワットとなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/8b847b9a-9156-cc77-a40d-a682ba8c3232.png)

## ③電力(POWER)および温度(TEMP)について直近4時間分の過去データから計算した移動平均を持つ列の作成

次に移動平均の特徴量を生成してみます。
_DIFFのノードをコピーして利用します。
右クリックして、「ノードをコピー」を選んでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/902732f2-2c3d-5d7e-6814-079567a8864b.png)

そしてキャンバスに貼り付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/1de9e548-c838-0863-f4d5-421802a93574.png)

DIFF」ノードと「ERR_CD_5FUTURE」ノードの間に入れてから、ダブルクリックしてプロパティを開き、以下を設定します。

フィールド名拡張子：_5MAVG

If:M_CD = <span>@<span>OFFSET(M_CD,4)
Then:<span>@<span>MEAN(<span>@<span>FIELD,5)
Else:undef

<span>@<span>MEANは、現在の行を始点にN行前までのデータの平均を計算します。
例えば、4行前から現在の行までのデータの平均を計算したい場合、<span>@<span>MEANの引数として5（=4（過去の行数）+1（現在の行））を指定します。

M_CD が4行前と同じ場合に、POWERとTEMPについて自行から4行前までのデータの平均を設定し、M_CD が4行前と違う場合はNULL（undef)を設定するという意味です。
設定ができたらOKで閉じてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/082e0bc4-171f-b544-1350-06baa1a4f529.png)

テーブルノードを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/f13eee21-b4b9-0838-5425-f5d8bb31a3e5.png)

POWER_5MAVGとTEMP_5MAVGという列が生成されています。
4時間目のPOWER_5MAVGは0時間目からの4時間目までの945,941,941,941,940ワットの平均である941.6ワットとなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/7c41a880-a79d-81d3-d760-700f07b26c94.png)


#3. pandasでの設定
## ①5時間後のエラーコード(ERR_CD)を持つ列の作成

同じデータ加工をpandasでもやってみます。

まず、csvファイルをデータフレームに読み込み、M_CDとUP_TIMEで昇順にソートしておきます。
また、M_CDとERR_CDは文字列型に変換しておきます。

```python
import pandas as pd
import numpy as np

df = pd.read_csv('/data/Cond4n_e.csv') 
df = df.sort_values(by=['M_CD','UP_TIME']).reset_index(drop=True) 
df['M_CD'] = df['M_CD'].astype(str)
df['ERR_CD'] = df['ERR_CD'].astype(str)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/067b18ba-ffbb-2b10-886b-d0f1b8335fec.png)


次に、pandasのshift()メソッドを利用し、5時間後のエラーコード(ERR_CD)を持つ列を作成します。

データは1時間周期で記録されているため、5行後のERR_CDの値が5時間後のエラーコード(ERR_CD)となります。
従って、5行後のERR_CDを持つ列を作成します。作成する列名は、ERR_CD_5FUTUREです。

shift()メソッドはデフォルトで前方（上方向）を参照します。
そのため、shift(5) とした場合、5行前を参照します。
今回は後方（下方向）を参照したいので、shift(-5) のように負の値を指定します。負の値を指定すると後方参照となります。

ただし、単純に5行後のERR_CDを参照して新しい列とすることは適切ではありません。
データには複数のマシン(M_CD)が含まれているため、あるマシンから別のマシンに切り替わるタイミングが存在します。
別のマシンのERR_CDを参照しないように、同じマシンの範囲においてのみ5行後のデータを参照する必要があります。

pandasのgroupby()メソッドを組み合わせることで実現することができます。
groupby()メソッドによってマシン(M_CD)の値が共通の行をグループ化し、グループ内でのみ5行後を参照します。

```python
df['ERR_CD_5FUTURE'] = df.groupby(['M_CD'])['ERR_CD'].shift(-5)
```

以下のように、5行後のERR_CDを参照して、ERR_CD_5FUTURE列 が作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/597d4f91-feba-a0c7-cb45-8bf7a2ce0c56.png)

また、あるマシンのデータの末尾5行は、5行後を参照できないためNaNが入ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/664e1f77-43ba-f84f-2895-9f257fc5d06c.png)

- 参考
 - [Pandas: Shift down values by one row within a group](https://stackoverflow.com/questions/26280345/pandas-shift-down-values-by-one-row-within-a-group)
 - [pandasでデータを行・列（縦・横）方向にずらすshift](https://note.nkmk.me/python-pandas-shift/)

## ②電力(POWER)および温度(TEMP)について1時間前の測定値からの差分を持つ列の作成

時系列データで差分を計算するときは、pandasのdiff()メソッドを使用します。
diff()メソッドはデフォルトで前方（上方向）との差分を算出します。
今回は1時間前との差分を持つ列を作成したいので、diff(1)とします。

上述のshiftと同様に別のマシンのERR_CDを参照しないよう、pandasのgroupby()メソッドを組み合わせて同じマシンの範囲においてのみ差分を計算するようにします。

```python
df['POWER_DIFF'] = df.groupby(['M_CD'])['POWER'].diff(1)
df['TEMP_DIFF'] = df.groupby(['M_CD'])['TEMP'].diff(1)
```

以下のように、1行前との差分を持つ、POWER_DIFF列とTEMP_DIFF列が作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/88276fc4-fc1e-4c9d-6e06-a9e1218b29c8.png)

前方を参照して差分を計算する都合上、先頭1行は前方を参照して差分を計算できないためNaNとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/30fbacb7-bed5-70fb-4f63-ef0805c89331.png)

- 参考
 - [pandasで行・列の差分・変化率を取得するdiff, pct_change](https://note.nkmk.me/python-pandas-diff-pct-change/)
 
## ③電力(POWER)および温度(TEMP)について直近4時間分の過去データから計算した移動平均を持つ列の作成

次に移動平均の特徴量を生成してみます。

時系列データで移動平均を計算するときは、pandasのrolling()メソッドとmean()メソッドを使用します。
rolling()メソッドで、現在の行を含むN行前までの範囲を指定します。
そして、指定した範囲に対しmean()メソッドで、平均を計算します。
今回は4時間前から現在までの平均を作成したいので、rolling(5).mean()とします。

上述のshiftと同様に別のマシンのERR_CDを参照しないよう、pandasのgroupby()メソッドを組み合わせて同じマシンの範囲においてのみ移動平均を計算するようにします。

ただし、groupby()メソッドへrolling()メソッドと集計メソッドを組み合わせた場合、①②と同じ方法で記載しただけでは、列への代入でエラーが発生します。これはデータがMultiindex形式で返されるためです。

＜エラー例＞

```shell-session
df['POWER_5MAVG'] = df.groupby(['M_CD'])['POWER'].rolling(5).mean()
(略)
TypeError: incompatible index of inserted column with frame index
```

下記のようにMultiindex形式でデータが返るため、1列に代入することができません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/e1b570ce-3a2c-1da7-c52f-cbb953ac8ae6.png)

そのため、groupby()メソッドについて①②で記載した内容に追加で、group_keys=False というオプションを指定します。
そして、作成された移動平均を含むデータフレームから、移動平均の計算に使用した列名を取り出します。

```python
df['POWER_5MAVG'] = df.groupby(['M_CD'], group_keys=False).rolling(5).mean()['POWER']
df['TEMP_5MAVG'] = df.groupby(['M_CD'], group_keys=False).rolling(5).mean()['TEMP']
```

以下のように、4行前から現在の行までの移動平均を持つ、POWER_5MAVG列とTEMP_5MAVG列が作成されています。
また、前方を参照して差分を計算する都合上、先頭4行は前方を参照して移動平均を計算できないためNaNとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237849/74eb6072-3230-f50e-44ba-81fb0942eddd.png)

- 参考
 - [pandasで窓関数を適用するrollingを使って移動平均などを算出](https://note.nkmk.me/python-pandas-rolling/)


#4. サンプル

ストリーム（[SPSS Modelerで時系列データをつかった故障分析②故障予知](https://qiita.com/kawada2017/items/a7fa843e166dfa98ec73)より）
https://github.com/hkwd/200414FailureAnalysisWithTimeSeries/archive/master.zip

Jupyter Notebook
URL更新予定

データ
https://raw.githubusercontent.com/hkwd/200611Modeler2Python/master/data/sampletranDEPT4en2019S.csv

■テスト環境
Modeler 18.2.1
Windows 10 64bit
Python 3.6.9
pandas 0.24.1