はじめに
2本立ての記事で、「東証1部の銘柄コードに対して翌日の株価トレンド(上昇/下降)を予測する」過程をCode交えて紹介しています。
本稿は、その2本目です。
- モデル作成に使うCSVファイルの準備
- CSVファイルをInputにした予測モデルの作成 ★本稿
Kaggleなどのデータ分析コンペだと、最近はLightGBMを利用して分析する傾向が強いと聞いたことがあります。
そのため、個人的には初挑戦ですが、LightGBMを使ってゆきます!
本稿で紹介すること
- 予測モデルの作成(含むハイパーパラメータのチューニング)
- 予測精度の確認
- 特徴量重要度の確認(可視化)
本稿で紹介しないこと
- データの収集および整形
- 特徴量設計
早速、予測モデルの作成(含むハイパーパラメータのチューニング)
こちらのサイトで公開されているPythonコード1を参考にしました。
予め作成したCSVファイルを読み込み、LightGBMのInputにしてゆきます。
その際、CSVに含まれるいくつからの列(高値/安値/始値/終値/出来高など)は削りました。目的変数は終値から導出していたりで、特徴量の多くが終値に関係するのですが、本稿では敢えて削らずInputとして使用しました。
# 不要カラムの削除と並べ替え
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を使わず、日付を基準に手作業で分割しました。
これは、過去と未来を明確に分割するためです。(興味のある方は、情報リークというキーワードで検索をば)
# 学習データを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関数の最後でタプルにして返しています。
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())
そして、乱数シードを指定します。
筆者は、素数を取得するワンライナーを使いました。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)
乱数シードの数だけ、実行します。
# 各試行での情報を保存
result = []
for seed in seeds:
print('### seed:%s ###' %seed)
result.append(autotuning(seed))
つらつらとログが流れます。。。
### 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コードで結果を集計します。
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%の差が生じることがあることも分かりました。
max: 0.6
min: 0.49387755102040815
ave: 0.5535982814178303
さらに、以下のPythonコードで最も良かった結果(およびその際の設定値)を取得します。
# accracy_score値で降順にソート
result = sorted(result[:], key=lambda x: x[1], reverse=True)
result[0]
ちなみに、最も良かった結果はと言うと。
乱数シードが67の時が、最も良かったという結果に。
(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を参考にしました。
以下のPythonコードでROC曲線と混合行列を確認しました。
ほぼ原文ままで、変数名とAPI参照を直したのみです。
# 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()
実行すると、こんな感じ。精度がもっと高いと、形も変わるはず。
最後、特徴量重要度の確認(可視化)
特徴量重要度の確認ですが、lightgbm.plot_importanceを使いました。
graphvizを使って決定木を可視化することも考えたのですが、全ての決定木(の中身)を正直見ていられないので、全体を俯瞰できる可視化方式が現実的です。
以下の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の方を重視して判断するのが良いかと思われます。
実行すると、こんな感じ。はやり先人たちの導き出した指標(特徴量)は、すごいですね。
Notebook(Pythonコード)
githubで公開しました。このタイミングですが、参考サイトの方々に感謝申し上げます。
まとめ
予測モデルの作成&チューニング、そして精度や特徴量の重要度を確認する方法までをご紹介。
世の中のデータサイエンティストは、Pythonを駆使してデータから価値を見出しているのですね、、、本当にすごい。