ゼロから始める株取引！Pythonで株価トレンド予測（２本目：予測モデルの作成から）

# はじめに

2本立ての記事で、「**東証1部の銘柄コードに対して翌日の株価トレンド（上昇／下降）を予測する**」過程をCode交えて紹介しています。
本稿は、その2本目です。

1. [モデル作成に使うCSVファイルの準備](https://qiita.com/Blaster36/items/9bdf777f35fda3335f6a)
2. CSVファイルをInputにした予測モデルの作成　★本稿

Kaggleなどのデータ分析コンペだと、最近はLightGBMを利用して分析する傾向が強いと聞いたことがあります。
そのため、個人的には初挑戦ですが、LightGBMを使ってゆきます！

## 本稿で紹介すること

- 予測モデルの作成（含むハイパーパラメータのチューニング）
- 予測精度の確認
- 特徴量重要度の確認（可視化）

## 本稿で紹介しないこと

- データの収集および整形
- 特徴量設計

# 早速、予測モデルの作成（含むハイパーパラメータのチューニング）

こちらのサイトで公開されているPythonコード[^1]を参考にしました。

https://note.com/10mohi6/n/n46d1bb0267b7

予め作成したCSVファイルを読み込み、LightGBMのInputにしてゆきます。
その際、CSVに含まれるいくつからの列（高値／安値／始値／終値／出来高など）は削りました。目的変数は終値から導出していたりで、特徴量の多くが終値に関係するのですが、本稿では敢えて削らずInputとして使用しました。

```python
# 不要カラムの削除と並べ替え
df = df[['weekday',
         #'High', 'Low', 'Open', 'Close',
         'Close_ratio', 'Body', 'Force_Index',
         'sma3', 'sma5', 'sma25', 'sma50', 'sma75', 'sma100',
         'upper1', 'lower1', 'upper2', 'lower2', 'upper3', 'lower3',
         'macd', 'macdsignal', 'macdhist',
         'rsi9','rsi14',
         'Up']]
df.dropna()
df
```

それから、異なるところを順番に。

データの分割ですが、sklearn.model_selection.train_test_splitを使わず、日付を基準に手作業で分割しました。
これは、過去と未来を明確に分割するためです。（興味のある方は、情報リークというキーワードで検索をば）

https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html

```python
# 学習データを2018-01-01～2020-12-31の期間とし、df_trainに入力する
df_train = df['2018-01-01':'2020-12-31']
df_train

# 検証データを2021-01-01以降とし、df_valに入力する
df_val = df['2021-01-01':]
df_val
```

また、ハイパーパラメータのチューニングを複数回試行すべく、乱数シードを指定しながら一気に実行できるように関数化しました。
objective関数は参考にさせていただいたサイト掲載の原文ままですが、以下のPythonコードで、各乱数シードでの試行結果（最適化パラメータ）を保存するため、autotuning関数の最後でタプルにして返しています。

```python
def objective(trial):
    dtrain = lgb.Dataset(X_train, label=y_train)
    param = {
        "objective": "binary",
        "metric": "binary_logloss",
        "verbosity": -1,
        "boosting_type": "gbdt",
        "lambda_l1": trial.suggest_float("lambda_l1", 1e-8, 10.0, log=True),
        "lambda_l2": trial.suggest_float("lambda_l2", 1e-8, 10.0, log=True),
        "num_leaves": trial.suggest_int("num_leaves", 2, 256),
        "feature_fraction": trial.suggest_float("feature_fraction", 0.4, 1.0),
        "bagging_fraction": trial.suggest_float("bagging_fraction", 0.4, 1.0),
        "bagging_freq": trial.suggest_int("bagging_freq", 1, 7),
        "min_child_samples": trial.suggest_int("min_child_samples", 5, 100),
    }

    gbm = lgb.train(param, dtrain)
    preds = gbm.predict(X_valid)
    pred_labels = np.rint(preds)
    accuracy = accuracy_score(y_valid, pred_labels)
    return accuracy

def autotuning(seed=31):
    #tuner = optuna.create_study(direction="maximize")
    # "sampler"オプションで乱数シードを指定
    tuner = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=seed))
    tuner.optimize(objective, n_trials=100)

    clf = lgb.LGBMRegressor(**dict(tuner.best_trial.params.items()))
    clf.fit(
        X_train,
        y_train,
    )

    y_pred = clf.predict(X_valid)
    print('accuracy_score: %s' %accuracy_score(y_valid, y_pred > 0.5))
    print('f1_score      : %s' %f1_score(y_valid, y_pred > 0.5, average='macro'))
    # Tupleとして戻り値に
    return (seed, accuracy_score(y_valid, y_pred > 0.5), f1_score(y_valid, y_pred > 0.5, average='macro'), tuner.best_trial.params.items())
```

そして、乱数シードを指定します。

:::note info
筆者は、素数を取得するワンライナーを使いました。Python好きなら一度は目にしたであろう、Pythonコードです。
:::

```python
import math

# 乱数シードを指定（from 素数を取得するワンライナー）
seeds = (lambda n:[x for x in range(2, n) if not 0 in map(lambda z: x%z, range(2, x))])(200)
```

乱数シードの数だけ、実行します。

```python
# 各試行での情報を保存
result = []

for seed in seeds:
    print('### seed:%s ###' %seed)
    result.append(autotuning(seed))
```

つらつらとログが流れます。。。

```shell
### seed:197 ###
[LightGBM] [Warning] lambda_l1 is set=3.6793172207340074e-05, reg_alpha=0.0 will be ignored. Current value: lambda_l1=3.6793172207340074e-05
[LightGBM] [Warning] feature_fraction is set=0.9167611168439482, colsample_bytree=1.0 will be ignored. Current value: feature_fraction=0.9167611168439482
[LightGBM] [Warning] bagging_freq is set=4, subsample_freq=0 will be ignored. Current value: bagging_freq=4
[LightGBM] [Warning] lambda_l2 is set=3.156660731044649e-06, reg_lambda=0.0 will be ignored. Current value: lambda_l2=3.156660731044649e-06
[LightGBM] [Warning] bagging_fraction is set=0.5476860914163431, subsample=1.0 will be ignored. Current value: bagging_fraction=0.5476860914163431
accuracy_score: 0.5551020408163265
f1_score      : 0.5540321637915199
### seed:199 ###
[LightGBM] [Warning] lambda_l1 is set=4.317390075261953e-08, reg_alpha=0.0 will be ignored. Current value: lambda_l1=4.317390075261953e-08
[LightGBM] [Warning] feature_fraction is set=0.896025570472924, colsample_bytree=1.0 will be ignored. Current value: feature_fraction=0.896025570472924
[LightGBM] [Warning] bagging_freq is set=4, subsample_freq=0 will be ignored. Current value: bagging_freq=4
[LightGBM] [Warning] lambda_l2 is set=0.04000145119601268, reg_lambda=0.0 will be ignored. Current value: lambda_l2=0.04000145119601268
[LightGBM] [Warning] bagging_fraction is set=0.5428686978287163, subsample=1.0 will be ignored. Current value: bagging_fraction=0.5428686978287163
accuracy_score: 0.5673469387755102
f1_score      : 0.5669934640522876
```

全試行が終了したら、以下のPythonコードで結果を集計します。

```python
result_acc = [x[1] for x in result]
print('max: %s' %max(result_acc))
print('min: %s' %min(result_acc))
print('ave: %s' %(sum(result_acc) / len(result_acc)))
```

ちなみに、集計結果はと言うと。
ハイパーパラメータのチューニングなしに自然体で実行すると、Accracyは54~55%くらいになるので、平均は妥当かなという印象でした。一方で、ハイパーパラメータのチューニングをしようとも、乱数シード次第では約10%の差が生じることがあることも分かりました。

```shell
max: 0.6
min: 0.49387755102040815
ave: 0.5535982814178303
```

さらに、以下のPythonコードで最も良かった結果（およびその際の設定値）を取得します。

```python
# accracy_score値で降順にソート
result = sorted(result[:], key=lambda x: x[1], reverse=True)
result[0]
```

ちなみに、最も良かった結果はと言うと。
乱数シードが67の時が、最も良かったという結果に。

```shell
(67,
 0.6,
 0.5787719298245614,
 dict_items([('lambda_l1', 2.2941800390458785e-05), ('lambda_l2', 0.06967340192569912), ('num_leaves', 13), ('feature_fraction', 0.7983380118574861), ('bagging_fraction', 0.6584846647743906), ('bagging_freq', 5), ('min_child_samples', 82)]))
```

# 次に、予測精度の確認

最も良かった結果に対して、精度に関して詳しく確認してゆきます。
こちらのサイトで公開されているPythonコード[^2]を参考にしました。

https://qiita.com/kt38k/items/3c0ee4251475b6407007

以下のPythonコードでROC曲線と混合行列を確認しました。
ほぼ原文ままで、変数名とAPI参照を直したのみです。

```python
# AUCを計算
fpr, tpr, thresholds = roc_curve(np.asarray(y_valid), y_pred)
auc = auc(fpr, tpr)
print("AUC", auc)

# ROC曲線をプロット
plt.plot(fpr, tpr, label='ROC curve (area = %.2f)'%auc)
plt.legend()
plt.title('ROC curve')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.grid(True)
plt.show()

# accuracy, precisionを計算
acc = accuracy_score(np.asarray(y_valid), np.round(y_pred))
precision = precision_score(np.asarray(y_valid), np.round(y_pred))
print("accuracy", acc)
print("precision", precision)

# 混同行列をプロット
y_pred = np.round(y_pred)
cm = confusion_matrix(np.asarray(y_valid), np.where(y_pred < 0.5, 0, 1))
cmp = ConfusionMatrixDisplay(cm, display_labels=[0,1])
cmp.plot(cmap=plt.cm.Blues)
plt.show()
```

実行すると、こんな感じ。精度がもっと高いと、形も変わるはず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/df5d34e5-ca03-5296-2783-9fbd77713e83.png)

# 最後、特徴量重要度の確認（可視化）

特徴量重要度の確認ですが、lightgbm.plot_importanceを使いました。
graphvizを使って決定木を可視化することも考えたのですが、全ての決定木（の中身）を正直見ていられないので、全体を俯瞰できる可視化方式が現実的です。

https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.plot_importance.html

以下のPythonコードで重要度の高い特徴量から可視化しました。

```python
# 重要度としては「特徴量が分岐（ノード）の条件式で使用された回数」（＝デフォルト）
lgb.plot_importance(clf, figsize=(30, 15), max_num_features=30, importance_type='split')

# 重要度としては「特徴量がある分岐（ノード）において目的関数の改善に寄与した度合い」
lgb.plot_importance(clf, figsize=(30, 15), max_num_features=30, importance_type='gain')
```

特に、2つ目の方式、gainの方を重視して判断するのが良いかと思われます。
実行すると、こんな感じ。はやり先人たちの導き出した指標（特徴量）は、すごいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/6f583d72-1ef4-2b51-e201-aa7692affb1a.png)

# Notebook（Pythonコード）

githubで公開しました。このタイミングですが、参考サイトの方々に感謝申し上げます。

https://github.com/Blaster36/Script/blob/main/StockPrice/predStockPrice-LightGBM2.ipynb

# まとめ

予測モデルの作成＆チューニング、そして精度や特徴量の重要度を確認する方法までをご紹介。
世の中のデータサイエンティストは、Pythonを駆使してデータから価値を見出しているのですね、、、本当にすごい。

[^1]: ファイル作成 | https://note.com/10mohi6/n/n46d1bb0267b7
[^2]: モデルの評価 | https://qiita.com/kt38k/items/3c0ee4251475b6407007#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E8%A9%95%E4%BE%A1