「LightGBMで決算プレイをやってみたけど、なんか精度が上がらない。。。XGBoostってどうなんだろう?」 → 子供の昼寝時間を使って試してみた。正直、大して期待していなかったけど、同じ製造業銘柄で比較してみたら面白い違いが出てきた。今日はその結果を全部公開する。
なぜXGBoostを試したか
以前、LightGBMで製造業銘柄の決算前後の動きを予測する記事を書いた。結果は正解率56〜58%くらいで、「ランダムよりちょっとマシ」という微妙な感じだった。Kaggleなどの機械学習コンテストを調べていると、金融系のタスクでXGBoostがよく使われているという記事を読んで気になっていた。
特徴量の種類や数を増やすよりも、まずアルゴリズムを変えて比較してみるのが正攻法だと思って実験してみた。
XGBoostとLightGBMの簡単な違い
両方とも「勾配ブースティング」系のアルゴリズムで、決定木を組み合わせて精度を高める手法だ。主な違いは:
XGBoost:木を「深さ優先」で成長させる。より細かいパターンを拾いやすい反面、過学習しやすい。
LightGBM:木を「葉優先」で成長させる。学習が速く、大規模データに強い。
株価予測のような「データ量が少なめ・ノイズが多い」場面では、どちらが有利かは試してみないとわからない。
実装:特徴量の作り方
まずはyfinanceでデータを取得して、テクニカル指標を特徴量に変換する。
import yfinance as yf
import pandas as pd
import numpy as np
from xgboost import XGBClassifier
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import accuracy_score, roc_auc_score
def add_features(df):
"""
テクニカル指標を特徴量として追加する。
翌日の騰落を予測するので、すべて当日終了時点の情報のみ使う。
"""
df = df.copy()
c = df['Close']
# モメンタム系
df['ret_1d'] = c.pct_change(1) # 前日比
df['ret_5d'] = c.pct_change(5) # 5日リターン
df['ret_20d'] = c.pct_change(20) # 20日リターン
# 移動平均
df['ma5'] = c.rolling(5).mean()
df['ma20'] = c.rolling(20).mean()
df['ma5_dev'] = (c - df['ma5']) / df['ma5'] # 5日乖離率
df['ma20_dev'] = (c - df['ma20']) / df['ma20'] # 20日乖離率
# RSI(14日)
delta = c.diff()
gain = delta.where(delta > 0, 0).rolling(14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
rs = gain / loss
df['rsi14'] = 100 - (100 / (1 + rs))
# ボラティリティ
df['vol_20d'] = c.pct_change().rolling(20).std()
# 出来高比
df['vol_ratio'] = df['Volume'] / df['Volume'].rolling(20).mean()
# ターゲット:翌日が前日比+0.5%以上なら1、それ以外は0
df['target'] = (c.shift(-1) / c - 1 > 0.005).astype(int)
return df.dropna()
# トヨタ(7203)・デンソー(6902)・キーエンス(6861)を対象に
tickers = ['7203.T', '6902.T', '6861.T']
dfs = []
for t in tickers:
raw = yf.Ticker(t).history(period="3y")
raw['ticker'] = t
dfs.append(add_features(raw))
df_all = pd.concat(dfs).sort_index()
print(f"データ数: {len(df_all)}")
print(df_all[['ret_1d', 'rsi14', 'vol_ratio', 'target']].tail())
XGBoostでモデルを訓練する
時系列データなので、通常のランダム分割ではなくTimeSeriesSplitを使う。これをやらないと「未来のデータで過去を予測する」という情報リークが起きてしまう。
feature_cols = [
'ret_1d', 'ret_5d', 'ret_20d',
'ma5_dev', 'ma20_dev',
'rsi14', 'vol_20d', 'vol_ratio'
]
X = df_all[feature_cols]
y = df_all['target']
# TimeSeriesSplitで時系列を守りながら交差検証
tscv = TimeSeriesSplit(n_splits=5)
acc_scores = []
auc_scores = []
for fold, (train_idx, val_idx) in enumerate(tscv.split(X)):
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
model = XGBClassifier(
n_estimators=200,
max_depth=4,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
eval_metric='logloss',
random_state=42,
verbosity=0
)
model.fit(X_train, y_train,
eval_set=[(X_val, y_val)],
verbose=False)
y_pred = model.predict(X_val)
y_prob = model.predict_proba(X_val)[:, 1]
acc = accuracy_score(y_val, y_pred)
auc = roc_auc_score(y_val, y_prob)
acc_scores.append(acc)
auc_scores.append(auc)
print(f"Fold {fold+1}: Accuracy={acc:.3f}, AUC={auc:.3f}")
print(f"\n平均 Accuracy: {np.mean(acc_scores):.3f}")
print(f"平均 AUC: {np.mean(auc_scores):.3f}")
実際に動かした結果(参考値):
Fold 1: Accuracy=0.541, AUC=0.538
Fold 2: Accuracy=0.558, AUC=0.561
Fold 3: Accuracy=0.549, AUC=0.554
Fold 4: Accuracy=0.562, AUC=0.567
Fold 5: Accuracy=0.553, AUC=0.558
平均 Accuracy: 0.553
平均 AUC: 0.556
LightGBMと比較してみた
同じ特徴量・同じデータでLightGBMを動かすと、平均Accuracyは0.547、AUCは0.550くらいだった。XGBoostがわずかに上回る結果になったけど、正直「誤差の範囲」という感じだ。
重要な特徴量(Feature Importance)を見てみると、どちらのモデルでもrsi14(RSI14日)とret_5d(5日リターン)が上位に来ていた。モメンタムと相対水準が製造業株の短期予測に効いているということかもしれない。
機械学習で株価予測をする上で注意したこと
①正解率55%でも意味があるか?
株は取引コスト(スプレッド・手数料)があるので、正解率が50%を少し超えるだけでは実際に利益は出ない。このモデルは「研究・学習用」と割り切っている。
②過学習に注意
XGBoostは`max_depth`が深すぎると簡単に過学習する。4〜5程度に抑えて、検証データでのAUCが訓練データのAUCから大きく乖離しないか確認すること。
③ターゲット設定が難しい
今回は「翌日+0.5%以上」を1とした。この閾値を変えるだけで正解率も変わる。正解率を上げたいなら閾値を高くすれば上がるけど、それはシグナルの数が減るだけで意味がない。
まとめ+次にやること
XGBoostとLightGBMの差は思ったより小さかった。正直、アルゴリズムを変えるよりも特徴量の質を上げることの方が重要だと実感した。次は決算データやセクター情報を特徴量に加えて、製造業株に特化した予測を試してみようと思う。
個人的な感想として、機械学習で株価を予測するのは「予測精度を上げること自体が目的」になりがちで、本当に大事なのは「その予測をどう実際のトレードに活かすか」だと気づいた。正解率55%のモデルでも、リスク管理と組み合わせれば十分使えるかもしれない。。。そこを次の課題にしたい。

