【AI投資システム構築】2.データの前処理方法

# データの前処理方法

前回の投稿「1. 学習用データの集め方」にて、為替レート（Open, Close, High, Low, Volume）の時系列データがCSVファイル等で入手できていることとします。

https://qiita.com/marcy_st_tech/items/ed8908fe6fdb2afae9d3

このデータに対して、前処理を行います。

::: note warn
学習データの前処理は、分析対象によっては「前処理」だけで専門書が出版されるぐらい、とても大事な作業です。前処理のやり方によって、学習モデルの成否が大きく変わります。演算の誤りがないように、注意して行いましょう。
:::

大きく分けて、3つの処理を実施します。

1. データクレンジング
1. 変化量へ変換
1. スケーリング

プログラミング言語は**Python3**を用います。
**NumPy**や**Pandas**等のライブラリを使用すると、楽にデータ処理を行えます。
例えばPandasの場合、CSVファイルの読み込みは下記ソースコードのように、簡単に行えます。

~~~ python:CSVファイル読み込み
import pandas as pd

# 時系列データ（CSVファイル読み込み）
trade_df = pd.read_csv(filePath, index_col=False, usecols=['Time','Open','Close','High','Low','Volume'], encoding='UTF-16')
~~~
index_col, usecols, encoding は、読み込むCSVファイルの形式を確認し、適宜変更が必要です。

# 1. データクレンジング
不正データ（外れ値）や欠損データを補う処理を行います。

## 1.1. 外れ値の除去・補正
不正データ（外れ値）は、「2.変化量へ変換」のように、時系列データを変化量に変換後、
ヒストグラムにすると視覚的にわかりやすいです。外れ値があれば、下記図のようにわかるはずです。
Pythonに慣れている方は、Matplotlibライブラリ等を用いてご確認ください。
Excel等の表計算ソフトに慣れている人は、CSVファイルを直接開いて確認してもいいと思います。

![外れ値例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992567/ec924f46-6e6b-4287-ba1a-898d652a985e.png)

このような外れ値を含んでいる場合、
quantile（分位数）を用いて外れ値を含むレコード（同時刻レコード）を検知して、
レコードごと（同じ時刻のOpen, Close, High, Low, Volumeを全て）除去するか、
他の値で置換します。

#### 1.1.1. レコード除去の場合
学習用データが十分にある場合は、レコード除去が有効です。
正確なデータだけ使うようにしましょう。
Pandasを用いたソースコードの例は以下の通りです
~~~ python:外れ値を含んだレコード除去の例
# Open値ついて、95％の分位数よりも値が小さいデータを抽出してそれ以上の値を除外する
q = trade_df['Open'].quantile(0.95)
trade_df = trade_df[trade_df['Open'] < q]
~~~

#### 1.1.2. 外れ値を置換の場合
学習用データが少なくレコード除去したくない場合は、置換もできます。
時系列データの場合は、前後の時刻データからある程度予測ができるからです。

例えば **時刻 _t_ の間隔が短い場合**、下記数式が成り立つはずです。
~~~ math
Open(t) ≒ Close(t-1)
~~~
正確には違いますが、もし _Open_ 値が外れ値の場合、1時刻前の _Close_ 値を用いても問題ないでしょう。
また、 _High_ 値が外れ値の場合、下記数式で置き換えてもいいと思います。
~~~ math
High(t) ≒ \frac{High(t-1) + High(t+1)}{2}
~~~
こちらも正確には違いますが、**外れ値が同時刻付近で頻発していない場合**は、有効な手段かと思います。
Pandasを用いたソースコードの例は以下の通りです

~~~ python:外れ値を置換の例
q = trade_df['Open'].quantile(0.95)
outlier_df = trade_df[trade_df['Open'] < q]
outlier_list = outlier_df.index.tolist()

for index in outlier_list:
    # 1列目:'Open', 2列目:'Close'の場合、Close(t-1)で置換する
    trade_df.iloc[index, 1] = trade_df.iloc[index-1, 2]
~~~

::: note info
:coffee: **Coffee Break**
前回の投稿に記載した通り、一昔前（１０年ほど前）は、外れ値を含む為替の時系列データがたくさんWeb上に公開されていました。昨今はこのようなデータは少なくなったと実感しています。この節の処理は今後、使うことはないでしょう。
:::

## 1.2. 欠損データの除去・補正

不正データ（外れ値）は少なくなりましたが、欠損データはたびたびあります。
特に注意しないといけないのが、**時間の欠損**です。

次回の投稿で詳しく記述するつもりですが、
１つの通貨ペアの情報だけで予測を行うのは、情報量が少なすぎるため限界があります。
そのため複数の通貨ペアの情報や、相関のある市場データ（株価等）も組み合わせて学習データにします。

その際、時刻毎データを並べると、例えば下記の表のように、同一時刻で特定のデータが欠損していることが多々あります。特に週替わりにはたびたび発生します。
また、アメリカなどで行われているサマータイムにも注意してください。
市場の取引時間が変わるためです。

| Index | Time | USDJPY | EURUSD | AUDJPY |
|:-----------:|:-----------:|:------------:|:------------:|:------------:|
| 1| 2020/01/23 01:00 | 150.569 | 1.03749 | NaN |
| 2| 2020/01/23 01:05 | 150.579 | 1.03584 | 93.472 |
| 3| 2020/01/23 01:10 | 150.489 | 1.03798 | 93.480 |
| 4| 2020/01/23 01:15 | 150.864 | 1.03445 | 93.570 |
| 5| 2020/01/23 01:20 | 150.568 | 1.03545 | 93.611 |
| :| : | : | : | : |
| x| 2020/01/24 00:00 | 150.001 | NaN | NaN |

このような欠損データを含んでいる場合、上記「1.1. 外れ値の除去・補正」同様、
同時刻のレコードごと除去するか、1つ前の時刻レコードで置換します。

複数の時系列データの統合と、
欠損データを含むレコードを削除するときのソースコードの例は以下の通りです

~~~ python:時系列データの統合と欠損データを含むレコード削除の例

USECOLS = ['Time','Open','Close','High','Low','Volume'] #CSVファイルの全要素

df_A = pd.read_csv(csv_file_A, usecols=USECOLS,
                                parse_dates= ['Time']) # 時刻タイプに変換
df_B = pd.read_csv(csv_file_B, usecols=USECOLS,
                                parse_dates= ['Time']) # 時刻タイプに変換

# 時系列データの統合（Time列を基準とする）
df_AB = pd.merge(df_A, df_B, on='Time', how='outer')

# 欠損データを１つでも含む場合、レコード削除
df_AB = df_AB.dropna(how='any')
~~~

::: note alert
この時間の欠損に対する処理は、細心の注意を払うことをオススメします。
過去に、ソースコードを誤って、欠損データを**１時刻先の未来のデータ**で置換したことがあります。
その学習データを用いて学習モデルを生成したところ、**未学習の（はずの）未来の為替の予測精度が80%以上と、爆発的に向上したことがありました。** 当時は大いに喜んで「すごいものをつくってしまった」と興奮したのを覚えています。
また、間違いに気づくまでだいぶかかったため（数か月）、皆様は同じ間違いを起こさないよう、ご注意ください。
:::


# 2. 変化量へ変換
入手した時系列データを、機械学習で学習させるため、定常化させます。
具体的には、差分をとります。
これもPandasを用いると、とても簡単に実行できます。

~~~ python:差分化
df_AB = df_AB.diff()
~~~

以上です。ただし、0レコード目はすべてNaNデータとなりますので、削除してください。

# 3. スケーリング
最後に、機械学習で学習しやすいようにするため、「2. 変化量への変換」後のデータに対して数値変換を行い、平均や分散、最小値・最大値を変更します。USDJPYの変動が大きく、EURUSDの変動が小さいような、要素ごとの偏りを無くす目的があります。

ここで、スケーリングに関する一般的な用語を定義します。
* 標準化：平均0，分散1へ変換
* 正規化：0～1へ変換
* 白色化：要素間の相関を無くす変換

詳しくは専門書籍や他のWebサイトをご参照ください。
ちなみに私は下記の書籍で学びました。数学寄りの専門書です。

https://www.amazon.co.jp/%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92-%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88-%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%97%E3%83%AD%E3%83%95%E3%82%A7%E3%83%83%E3%82%B7%E3%83%A7%E3%83%8A%E3%83%AB%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-%E5%B2%A1%E8%B0%B7-%E8%B2%B4%E4%B9%8B/dp/4065133327/ref=sr_1_3?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=QWM9UYMS9HKQ&dib=eyJ2IjoiMSJ9.QtfHKrVv1kIGiv85uAUvu9OnkyXanfYmLs2xIOQxnJZ9dsBkiKPuPlLuPGJ0lBORDnXRaxaTylktgsCcEHqqMrC-FH2gFTkiFPZZLvkoQEjuwPrNFQitUWNL2-jZrRAiBs6r5bCQREHKMhr4IN05dFRCdyHf1VHrqIiry1XOjA8DrF3rf3_YgoFNvADISnhb8tb2A7-TkRaQlmsQrdK3ftHxToqg9TrxkO7k5syPHz4.9alA7RwwLtYveWjoXOLMHFdP8LHyJJgDYpEm6Gw5AG8&dib_tag=se&keywords=%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92&qid=1740798213&s=books&sprefix=%E6%B7%B1%E5%B1%A4%E5%AD%A6%E7%BF%92%2Cstripbooks%2C179&sr=1-3

どの手法を用いるのか、ここは個人の判断が分かれるところかと思います。
私の経験上、「1. データクレンジング」と「2. 変化量へ変換」まで完了したデータであれば、そこまで偏ったデータとはならず、通貨毎の特徴をなくしたくないと思い、あえて標準化のみを行っています。
（もしかしたら専門家の先生からしたら、それはダメだと言われるかもしれません）

標準化の数式は下記のとおりです。
~~~ math
平均：\bar{x} = \frac{1}{N} \sum_{n=1}^N x(n)
~~~
~~~ math
標準偏差：\sigma_{x} = \sqrt{\frac{1}{N} \sum_{n=1}^N (x(n) - \bar{x})^2}
~~~
~~~ math
x(t) ← \frac{x(t) - \bar{x}}{\sigma_{x}}
~~~

Pandasを用いたソースコードは下記のとおりです。
~~~ python:標準化
mean_val = df_AB .mean(axis=0) # 平均
df_AB  = df_AB  - mean_val
std_val = df_AB .std(axis=0) # 標準偏差
df_AB  = df_AB  / std_val
~~~

この変換後の時系列差分データをCSVファイル、またはNumpyファイルへ出力し、
以降の処理で用いています。

# まとめ
学習用データの前処理方法について記載しました。
何度も記載していますが、この処理は丁寧に行い、計算式に誤りがないか何度も確認しましょう。誤ったデータで学習させるのは本当に無駄です。

また、専門書があるように、私が記載した内容はすべてではありません。
データ前処理の方法について、少しでも参考にしていただけますと幸いです。

次は、「学習に用いるデータの選定方法」について、投稿する予定です。
よろしくお願いいたします。