2
1

More than 1 year has passed since last update.

ゼロから始める株取引!Pythonで株価トレンド予測(2本目:予測モデルの作成から)

Last updated at Posted at 2022-02-06

はじめに

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

  1. モデル作成に使うCSVファイルの準備
  2. 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()

実行すると、こんな感じ。精度がもっと高いと、形も変わるはず。
image.png

最後、特徴量重要度の確認(可視化)

特徴量重要度の確認ですが、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の方を重視して判断するのが良いかと思われます。
実行すると、こんな感じ。はやり先人たちの導き出した指標(特徴量)は、すごいですね。
image.png

Notebook(Pythonコード)

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

まとめ

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1