機械学習モデルの精度を上げる特徴量エンジニアリング入門【日本株製造業編】

Python実装・コード

以前にXGBoostで日本株の翌日騰落を予測しようとしたとき、「終値」「出来高」「RSI」だけを特徴量に入れたら精度がコイン投げと変わらなかった。。。ランダムフォレストに変えても同じ。「アルゴリズムが悪いんじゃなくて特徴量が悪いんだ」と気づくまでずいぶん遠回りした。

特徴量エンジニアリングが重要な理由

機械学習モデルの精度は「どんなモデルを使うか」より「どんな特徴量を入れるか」のほうが影響が大きいと言われている。生の株価データをそのまま入れても、モデルは「昨日の終値が140円だった」という情報からほとんど何も学べない。重要なのは「昨日の終値と20日移動平均の乖離率が+3%だった」というような相対的・派生的な情報なんだよね。

特に日本の製造メーカー株は、テクニカル系の特徴量に加えて「ドル円」「輸出関連の業種特性」を絡めた特徴量が効きやすいと感じている。トヨタやデンソーは円安で業績が上がる傾向があるので、ドル円の動きが株価に影響するのは直感的にも納得できる。

特徴量のカテゴリと実装

実際に使っている特徴量を5カテゴリに分けて紹介する。

import yfinance as yf
import pandas as pd
import numpy as np

def fetch_data(ticker, period="3y"):
    stock = yf.Ticker(ticker)
    df = stock.history(period=period)
    df = df[["Open", "High", "Low", "Close", "Volume"]].copy()
    df.index = pd.to_datetime(df.index)
    return df

# トヨタ自動車(例)
df = fetch_data("7203.T")

# ===== カテゴリ1: テクニカル指標系 =====
# 移動平均乖離率
for window in [5, 10, 20, 60]:
    ma = df["Close"].rolling(window).mean()
    df[f"ma_dev_{window}"] = (df["Close"] - ma) / ma * 100

# RSI
def calc_rsi(series, period=14):
    delta = series.diff()
    gain = delta.clip(lower=0).rolling(period).mean()
    loss = (-delta.clip(upper=0)).rolling(period).mean()
    rs = gain / loss
    return 100 - (100 / (1 + rs))

df["rsi_14"] = calc_rsi(df["Close"])
df["rsi_28"] = calc_rsi(df["Close"], 28)

# MACDとシグナル
ema12 = df["Close"].ewm(span=12, adjust=False).mean()
ema26 = df["Close"].ewm(span=26, adjust=False).mean()
df["macd"] = ema12 - ema26
df["macd_signal"] = df["macd"].ewm(span=9, adjust=False).mean()
df["macd_hist"] = df["macd"] - df["macd_signal"]

# ボリンジャーバンド幅
bb_mid = df["Close"].rolling(20).mean()
bb_std = df["Close"].rolling(20).std()
df["bb_width"] = (bb_std * 4) / bb_mid * 100  # バンド幅 = 2σ*2 / 中心

# ATR(ボラティリティ)
high_low = df["High"] - df["Low"]
high_close = (df["High"] - df["Close"].shift(1)).abs()
low_close = (df["Low"] - df["Close"].shift(1)).abs()
df["atr_14"] = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1).rolling(14).mean()
df["atr_pct"] = df["atr_14"] / df["Close"] * 100  # 価格比率に正規化

print("テクニカル特徴量作成完了:", [c for c in df.columns if c not in ["Open","High","Low","Close","Volume"]])
# ===== カテゴリ2: 価格モメンタム系 =====
# 過去N日リターン
for lag in [1, 3, 5, 10, 20]:
    df[f"return_{lag}d"] = df["Close"].pct_change(lag) * 100

# 高値・安値更新フラグ(直近Nバーの中で)
for window in [5, 20, 60]:
    df[f"new_high_{window}"] = (df["Close"] == df["High"].rolling(window).max()).astype(int)
    df[f"new_low_{window}"]  = (df["Close"] == df["Low"].rolling(window).min()).astype(int)

# 窓開け(ギャップ)
df["gap_pct"] = (df["Open"] - df["Close"].shift(1)) / df["Close"].shift(1) * 100

# 陽線・陰線・十字線の判定
df["candle_body"] = (df["Close"] - df["Open"]) / df["Open"] * 100
df["is_bullish"] = (df["Close"] > df["Open"]).astype(int)

print("モメンタム特徴量作成完了")
# ===== カテゴリ3: 出来高系 =====
# 出来高移動平均比
vol_ma20 = df["Volume"].rolling(20).mean()
df["vol_ratio_20"] = df["Volume"] / vol_ma20  # 1より大きければ出来高増加

# 価格×出来高(資金流入量の代理変数)
df["volume_price"] = df["Volume"] * df["Close"]
vp_ma10 = df["volume_price"].rolling(10).mean()
df["vp_ratio"] = df["volume_price"] / vp_ma10

# 出来高急増フラグ
df["vol_spike"] = (df["vol_ratio_20"] > 2.0).astype(int)  # 20日平均の2倍以上

print("出来高特徴量作成完了")
# ===== カテゴリ4: ドル円連動特徴量(製造業株特有)=====
usdjpy = fetch_data("JPY=X")

# ドル円の日次変化率
usdjpy["usdjpy_return_1d"] = usdjpy["Close"].pct_change() * 100
usdjpy["usdjpy_return_5d"] = usdjpy["Close"].pct_change(5) * 100
usdjpy["usdjpy_20ma_dev"]  = (usdjpy["Close"] - usdjpy["Close"].rolling(20).mean()) / usdjpy["Close"].rolling(20).mean() * 100

# 日付インデックスを揃えてマージ
usdjpy_features = usdjpy[["usdjpy_return_1d", "usdjpy_return_5d", "usdjpy_20ma_dev"]].copy()
usdjpy_features.index = usdjpy_features.index.tz_localize(None)
df.index = df.index.tz_localize(None)

df = df.join(usdjpy_features, how="left")

# ドル円と株価の相関(直近20日)
df["corr_usdjpy_20d"] = (
    df["return_1d"].rolling(20)
    .corr(df["usdjpy_return_1d"])
)

print("ドル円特徴量追加完了")
print(df[["usdjpy_return_1d", "usdjpy_20ma_dev", "corr_usdjpy_20d"]].tail(5))
# ===== カテゴリ5: カレンダー系(日本市場特有) =====
df["day_of_week"] = df.index.dayofweek      # 0=月曜, 4=金曜
df["month"] = df.index.month
df["is_month_end"]   = df.index.is_month_end.astype(int)
df["is_month_start"] = df.index.is_month_start.astype(int)

# 決算月フラグ(3月・9月末決算が多い製造業向け)
df["is_earnings_month"] = df["month"].isin([3, 9]).astype(int)

# 翌日の予測ターゲット(翌日終値が今日より上か下か)
df["target"] = (df["Close"].shift(-1) > df["Close"]).astype(int)

# 特徴量リスト
feature_cols = [c for c in df.columns if c not in ["Open","High","Low","Close","Volume","target"]]
print(f"特徴量数: {len(feature_cols)}")
print(feature_cols)

特徴量の重要度を確認する

特徴量を作ったら、実際にXGBoostに入れて重要度を確認するのが大事。闇雲に特徴量を増やすより、効いていない特徴量を削ったほうがモデルが安定することが多い。

from xgboost import XGBClassifier
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

# 欠損値削除
df_clean = df[feature_cols + ["target"]].dropna()
X = df_clean[feature_cols]
y = df_clean["target"]

# 時系列交差検証(時系列データはシャッフルNGなので必ずTimeSeriesSplit)
tscv = TimeSeriesSplit(n_splits=5)
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,
        random_state=42,
        eval_metric="logloss",
        verbosity=0,
    )
    model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)

    pred = model.predict(X_val)
    acc = accuracy_score(y_val, pred)
    scores.append(acc)
    print(f"Fold {fold+1}: {acc:.4f}")

print(f"\n平均精度: {np.mean(scores):.4f} ± {np.std(scores):.4f}")

# 最後のモデルで特徴量重要度を可視化
importances = pd.Series(model.feature_importances_, index=feature_cols)
importances = importances.sort_values(ascending=False).head(20)

plt.figure(figsize=(10, 7))
importances.plot(kind="barh", color="steelblue")
plt.title("特徴量重要度 Top20(XGBoost)")
plt.xlabel("Importance")
plt.gca().invert_yaxis()
plt.tight_layout()
plt.savefig("feature_importance.png", dpi=150)
plt.show()

効果的な特徴量の選び方:実感から

実際にトヨタやデンソーで試した経験から、効きやすいと感じた特徴量を挙げると、まず移動平均乖離率(特に20日と60日)は安定して重要度が高い。次にATR_pct(ATRを価格比率で正規化したもの)はボラティリティを捉えるのに使いやすい。出来高比率(vol_ratio_20)は価格サインの信頼性を測る補助特徴量として効果的だった。そしてドル円5日リターン(usdjpy_return_5d)は製造業に特有の効果があった。

逆に生の終値・出来高・日付のような「絶対値」は精度に寄与しにくい。必ず「何かに対する比率」や「変化率」に変換するのが鉄則だと思っている。

まとめ

今回紹介した特徴量エンジニアリングのポイントは5つ。テクニカル指標を乖離率・比率に正規化すること、モメンタム(過去リターン)を複数期間で取ること、出来高を平均比率に変換すること、製造業株ならドル円連動特徴量を追加すること、そしてカレンダー効果(月末・決算月)を忘れないことだ。

個人的には、特徴量の質を上げてから精度が53%→56%台まで上がってきた。まだコインの裏表を少し上回る程度だけど、これで満足せずに次は「業種別因子(例:日経平均との相関)」を追加して更に改善を試みる予定。機械学習の沼は深い。。。

タイトルとURLをコピーしました