ゼロから始めるAIシステム開発 #11 「為替予測モデル作成でPythonに慣れる」

## Python勉強してます
Pythonの機械学習が難しくも楽しくて日がな触ってる。とはいえ目的はちょっと別の地点なのでいいかげん区切りをつけて、この記事にて内容報告したいと思います。

## 為替予測モデルを作ってみよう
先週まで児童書みたいな入門書読んでたのにハードル高くない？とは思いましたが、その「[Python1年生](https://amzn.to/40WeTYx)」のラストが機械学習で、抱いていたイメージよりはるかに実装が楽だったので行けるのでは…とも思い敢行。一から実装できれば成果は大きそう。**最終的にはChatGPT大先生に聞けばどうにでもなる**だろう。

## 為替データを取得
まずは為替データを手に入れなければ話にならない。調べた結果APIを提供しているFX事業者は多くないようだが、Yahooファイナンスが無料で提供しているようなのでそこを使用。ちなみにAPI自体は生きているがサポート等は終了しているらしい。

```exchange_dataset01.py
import yfinance as yf

# 米ドル/日本円 為替データ取得
ticker = yf.Ticker('USDJPY=X')
# 5分足を60日分
df = ticker.history(interval='5m', period='60d')
# 表示
print(df)

# タイムゾーンを日本時間に変更
df.index = df.index.tz_convert("Asia/Tokyo")
print(df)

# csvに保存
df.to_csv("usdjpn_5m_241118_250207.csv")
```
すんなり行けた。青字のパラメータを変更することで各種株価を取得したり、ロウソク足の種類・期間等が変えられる。タイムゾーンがアメリカの時間なので日本時間に修正。機械学習に使うためのデータセットはいろいろ保存方法があるようだが、無難そうなCSVに保存。

## データセットを作成
機械学習で使えるようにデータを追加したり形を整えたりしていく。機械学習による未来の予測には「回帰（数字の予測）」や「分類」があるが、今回はPython1年生で学んだことを活かして「分類」でシンプルに上がるか下がるかを予測する。
**説明変数**（**特徴量**・分類するための手がかり）は複数必要で、**目的変数**（正解データ・分類したいもの）は一つ。分類の手法には**SVM（サポートベクターマシン）**と**K近傍法**を使用する。超ざっくり説明すると、SVMは空間に面を布いて目的の出力がどちら側にあるかで分類、K近傍法は目的の出力の周囲にあるものがどれかによって分類する手法。

```exchange_dataset02.py
import pandas as pd
import numpy as np

# csv読み込み
df = pd.read_csv('usdjpn_5m_241118_250207.csv')
df.info()

# データを整え特徴量を追加
print("時間のデータ型をdatetime64型に-----------------")
df['Datetime'] = pd.to_datetime(df['Datetime'])
print("時間帯とカラムのフィルタ-----------------------")
df = df[(df['Datetime'].dt.time >= pd.to_datetime('08:00').time()) & (df['Datetime'].dt.time <= pd.to_datetime('17:00').time())]
df = df.loc[:,'Datetime':'Close']
print("単純移動平均カラムを追加-----------------------")
df['SMA21'] = df['Close'].rolling(window=21, min_periods=1).mean()
print("最新位置のカラムを追加-----------------------")
df['Recent'] = df['Close'] - df['SMA21']
print("過去21最大位置カラムを追加-----------------------")
df['Top'] = df['High'].rolling(window=21, min_periods=1).max() - df['SMA21']
print("過去21最小位置カラムを追加-----------------------")
df['Bottom'] = df['Low'].rolling(window=21, min_periods=1).min() - df['SMA21']
print("過去21の平均カラムを追加-----------------------")
df['Integral'] = df['Recent'].rolling(window=21, min_periods=1).sum() / 21
print("単純移動平均の傾きカラムを追加-----------------------")
df['Slope'] = df['SMA21'] - df['SMA21'].shift(1)
print("2つ前値動きのカラムを追加-----------------------")
df['PPM'] = df['Close'].shift(1) - df['Open'].shift(1)
print("直近値動きのカラムを追加-----------------------")
df['RPM'] = df['Close'] - df['Open']    
print(df)

print("目的変数を計算し追加-----------------------")
df['Rise'] = df['High'] - df['Open'] # 上昇値
df['Fall'] = df['Open'] - df['Low'] # 下降値
df['Unidirect'] = df['Rise'] - df['Fall'] # 単方向性

# 単方向性の強さに応じて分類、1なら買い、-1なら売り
conditions = [
    df['Unidirect'].shift(-2) >= 0.025,  # x以上
    df['Unidirect'].shift(-2) > -0.025,  # -xより上 x未満
    df['Unidirect'].shift(-2) <= -0.025   # -x以下
]
choices = [1, 0, -1]
df['Target'] = np.select(conditions, choices, default=1)

print("不要なカラムを削除-----------------------")
df = df[(df['Datetime'].dt.time >= pd.to_datetime('11:00').time()) & (df['Datetime'].dt.time <= pd.to_datetime('15:50').time())]
df = df.drop('Rise',axis=1)
df = df.drop('Fall',axis=1)
df = df.drop('Unidirect',axis=1)
df = df.loc[:,'Recent':'Target']
print(df)

# csvに保存
df.to_csv("svmdataset_usdjpn_5m.csv")

# 以下は分析パート
import seaborn as sns
import matplotlib.pyplot as plt

# 目的変数の分布を可視化
sns.countplot(x=df['Target'])
plt.title("Target Distribution")
plt.show()

# 目的変数の割合を数値でも確認
print(df['Target'].value_counts(normalize=True))

# 目的変数との相関を計算
corr_matrix = df.corr()

# Target との相関が強い順に表示
print(corr_matrix['Target'].sort_values(ascending=False))

# 相関ヒートマップを可視化
plt.figure(figsize=(10, 6))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0)
plt.title("Feature Correlation with Target")
plt.show()

```
表形式を扱えるデータ解析支援のライブラリ、Pandasを駆使してデータセットを作っていく。機械学習をやるライブラリはscikit-learnだが、実装までの8割くらいはPandasとの格闘になった印象。使ったモジュールやメソッドは
- datetime 時間を参照して操作する
- loc 必要な行や列を抽出する
- rolling 複数の行を参照して操作する
- shift 現在の行から離れた別の行の内容を参照する
- drop 行列を削除する
- corr 相関を計算する

などなど。なんでもできる。特徴量として移動平均や一定範囲の過去最大値や最小値を求めるのに使用。分析自体はExcelのほうが直観的にできそうなのもあるが、これらに慣れればPythonでやるほうが早くなりそう。

## 学習して予測を実行
データができたので検証する。
データセットをX（特徴量）とy（目的変数）に分ける。（Xは大文字が慣例らしい。）
さらにデータセットのうちの8割を使って残りの2割が当てられるかを見るために8:2で分割。
さらにそれぞれの数値のスケールが違い過ぎるとうまく予測できないのでスケーリング（標準化・-1から1の間に収める処理）を行う。
SVMモデルを作成、学習、予測する。ｋ近傍法にするには作成の部分をちょっと変えるだけなので非常に便利。（よってコードは割愛）
```exchange_svm.py
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler

RANDSTATE = 11
print(f'RANDSTATE:{RANDSTATE}')

# csv読み込み
df = pd.read_csv('svmdataset_usdjpn_5m.csv')

# 特徴量Xと目的変数yに分ける
X = df.loc[:,'Recent':'RPM']
y = df['Target']  # 目的変数

# 学習データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=RANDSTATE)

# スケーリング
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)  # 訓練データのスケールを調整
X_test = scaler.transform(X_test)  

# SVMのモデル作成
model = SVC(kernel='rbf', C=1, gamma=0.01, random_state=RANDSTATE)
#model = SVC(kernel='linear', C=1.0, gamma='scale', random_state=RANDSTATE)
#model = SVC(kernel='poly', degree=3, random_state=RANDSTATE)
#model = SVC(kernel='sigmoid', gamma=0.03, coef0=0.2, random_state=RANDSTATE)

# 学習
model.fit(X_train, y_train)

# 予測
y_pred = model.predict(X_test)

# 予測結果を DataFrame にまとめる
df_results = pd.DataFrame({
    'Actual': y_test,   # 実際のクラス（目的変数）
    'Predicted': y_pred # SVMの予測結果
})
df_results['Correct'] = df_results['Actual'] == df_results['Predicted']

# 予測を表示
print(y_pred) # 予測内容
print(y_pred.shape) #予測回数

# 以下の条件で収益をカウント
# 0はスルー、1の時は買い、-1の時は売り。分類が的中したら+55円、外れたら-30円
df_results['profit'] = np.where(
    df_results['Predicted'] != 0,  # 'Predicted' が 0 でない場合
    np.where(df_results['Correct'], 55, -30),  # 'Correct' が True なら 55、False なら -30
    0  # 'Predicted' が 0 の場合は 0
)
# 'profit' 収益の合計を表示
print("Total Profit:", df_results['profit'].sum())

# 結果の確認
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(y_test, y_pred)
print(f'Accuracy: {accuracy:.2f}')
```
sklearn.svmでモデル作成しパラメータ設定、fitで学習、Predictで予想。データセットさえ準備できれば予測自体は3行で済んでしまう。すごい簡単。
## 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/a15ee3a2-cf9b-4c94-a0ee-da78c0485ff5.png)
テストデータによる検証結果。
1は強い上昇、0はあまり変わらず、-1は強い下降を予測している
696は予測回数、Total Profitは売り買いした場合の合計収益、Accuracyは的中確率。
3択なので的中確率や収益結果だけ見ると良さげに見えるが、**予測結果に明らかに-1が少ない。** たまたまかと思ったが8:2分割のランダム値（上のRANDSTATE）を変えてもこんな感じなので何かしら特徴量に不備がありそう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/624e4667-c319-41be-8c5d-01966b2c7ba5.png)
ｋ近傍法だとこんな感じ。データセットは同じなので、特徴量の中にSVMに合わないものが含まれているということだろうか？こちらは的中率こそ良いが収益がマイナス。おそらく0のときに0を予測して当てているのが的中率を稼いでいるだけっぽい。（値動きが微妙な時にスルーしているだけ）これでは使えない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/3d35cac6-b6c6-48ec-ac69-a49fdfe98bf0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/fa19b15b-10e2-465a-99f1-056c69773398.png)
グラフを描画するライブラリのmatplotlibを使用して原因を探る。上は目的変数の分布。3択が偏っているわけではない。下は特徴量や目的変数ごとの相関性を表したマップ。どうもすべての特徴量が目的変数と相関が低すぎるっぽい？高すぎてもいけないようだが…。
## おわりに
という感じで10日ほどずっと触ってました。SVMはいったんやめて**LSTM**というニューラルネットワークの手法に手を出して実装までこぎつけたけど、いい結果には至らず。まあ為替や株価予想なんてものがすんなりできるならもう働かなくていいのでそう簡単には行かないか。もっとやりたかったけど目的はそこじゃないのでこの辺で切り上げ。主にPandasとの格闘だったけどPythonに慣れることは出来たんじゃないだろうか。
あとChatGPT大先生のスゴさを、既に知ってたつもりだがさらに思い知った。何聞いても答えてくれるし、やりたいことを察してくれるので、まずChatGPTに投げて実装、コードを読み解いて修正、というやり方が基本になりそう。

さて次はどうしようかな。いつもの入門書に戻るか、アプリを作ってみるか…。