0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「このラップはなぜ遅い?」をSHAPで分解する — Random Forestの予測根拠を秒単位で読む

0
Posted at

レーシングシミュレータの実データを使って、データサイエンスの概念を実装とともに解説しています。この記事では SHAP値(SHapley Additive exPlanations) を扱います。


こんな人に役立ちます

  • Random Forestの「特徴量重要度」と「SHAP値」の違いを実データで理解したい
  • 「このサンプルの予測根拠」を変数ごとに分解する方法を知りたい
  • モデルの解釈可能性(Explainable AI)を実装レベルで学びたい

結論

富士スピードウェイ1コーナー(T1区間、500〜1000m)の94ラップを対象に、SHAP値でRandom Forestの予測根拠を分解しました。

  • 全体的な傾向throttle_pct__mean(平均|SHAP|= 0.218秒)が他変数を圧倒
  • 個別ラップの分解:最速ラップ(11.9秒)ではスロットル開度が-0.4秒以上の貢献(速い方向)、最遅ラップ(13.5秒)では同変数が+0.3秒以上の貢献(遅い方向)
  • Dependence Plot:スロットル開度が高いほどSHAP値が負(速い方向)に転じる右下がりのパターン
  • ベース値(全ラップ平均):12.326秒——各ラップのSHAP値の合計がベース値からのズレ

「throttle_pct__meanが重要」という結果は前記事でも得られていましたが、SHAP値はそこから一歩進んで 「このラップのタイムに、この変数がX秒分寄与した」 という個別の説明を与えます。

問い

前記事でRandom ForestのCV R²=0.848を達成し、throttle_pct__mean が特徴量重要度1位という結果を得ました。

ただし、特徴量重要度(Feature Importance)には限界があります。

特徴量重要度 = 全94ラップを通じた平均的な「重要さ」

「最遅ラップ(13.5秒)はなぜそう予測されたのか」「スロットル開度と横Gのどちらが、このラップのタイムを引っ張ったのか」——こうした 個別サンプルへの問い には答えられません。

SHAP値はこの問いに答えます。

この手法が有効な場面はモータースポーツに限りません。 「なぜこの顧客が離脱したのか」「なぜこの製品ロットに不良が出たのか」など、予測モデルを運用していて「このケースの理由が知りたい」場面に直接使えます。モデルが高精度でも、中身が不透明では判断や改善につながりにくい——SHAP値はそのブラックボックス問題へのアプローチです。

領域 モデルが予測するもの SHAPで知りたいこと
モータースポーツ セクタータイム このラップでどの操作が時間をロスしたか
EC・マーケティング 顧客の離脱確率 この顧客の離脱にどの変数が効いたか
製造業 製品の不良率 このロットの不良にどのパラメータが寄与したか
金融 与信スコア この申請の判断根拠を変数ごとに分解する

SHAP値とは何か

SHAP値は、ゲーム理論の Shapley値(複数のプレイヤーが協力して得た成果を、各プレイヤーの貢献量に応じて公平に分配するための指標)を機械学習モデルの解釈に応用したものです。各「変数」をプレイヤー、「予測値」を成果として見立て、変数ごとの貢献量を算出します。

直感的には次のように考えてください。

  • モデルは全ラップ平均タイム(12.326秒)を ベース値(変数の情報が何もない状態での予測値)として出発する
  • 各変数は、そのラップの実際の値に応じてベース値を「押し上げる(遅い方向)」か「押し下げる(速い方向)」か寄与する
  • 各変数のSHAP値を合算すると、モデルの予測値に一致する
予測値 = ベース値 + Σ(各変数のSHAP値)
      = 12.326秒 + SHAP_throttle + SHAP_lat_g + ... + SHAP_gear

特徴量重要度との主な違いは次の通りです。

特徴量重要度 SHAP値
単位 相対的な重要度(合計1) 秒(目的変数と同じ単位)
対象 全データの平均的な傾向 1サンプルごとの寄与
方向性 なし(大きいほど重要) あり(正=遅い方向、負=速い方向)
加算性 なし あり(合計 = 予測値 − ベース値)

一言で言えば、特徴量重要度は「この変数は全体的に大事」とだけ言えますが、SHAPは「このラップではこの変数が X 秒分タイムを引き上げた/引き下げた」と言えます。

なお、ここでいう「秒単位で分解する」とは、時系列を1秒ごとに分割するという意味ではなく、各特徴量の寄与を目的変数(タイム)と同じ「秒」の単位で読める、という意味です。

Random ForestにSHAP値を計算する方法として、shap.TreeExplainer が効率的です。Tree系のモデル(Random Forest、XGBoost、LightGBM等)に対して、モデルの構造を活用して効率よくSHAP値を計算できます。

データ準備

使用ライブラリ:pandas、numpy、scikit-learn、shap、matplotlib

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score, KFold
import shap

DATA_DIR = "/path/to/TelemetryData"
T1_START_M = 500   # T1区間開始(累積走行距離)
T1_END_M   = 1000  # T1区間終了
FPS        = 60.0

特徴量エンジニアリングの実装でT1区間を抽出し、各ラップを mean/max/min/std の集約特徴量に変換します。ドライバーが直接コントロールする6チャネル(throttle_pct、brake_pct、lat_g、lon_g、gear、wheel_rotation_radians)の4集約で24変数を使います。

# 外れ値除外後のデータ: n=94ラップ
X_op = df_clean[op_cols].values  # shape: (94, 24)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_op)
y = df_clean['t1_time'].values  # T1通過タイム(秒)

モデル構築

rf = RandomForestRegressor(n_estimators=300, random_state=42, n_jobs=-1)
kf = KFold(n_splits=5, shuffle=True, random_state=42)
cv_r2 = cross_val_score(rf, X_scaled, y, cv=kf, scoring='r2')
rf.fit(X_scaled, y)
print(f"CV R²: {cv_r2.mean():.3f}")
# → CV R²: 0.848

CV R²=0.848は「ドライバー起因の変数24個だけで、T1タイムの差の約85%を説明できる」精度です(詳細は前記事)。

SHAP値の計算

explainer = shap.TreeExplainer(rf)
shap_values = explainer.shap_values(X_scaled)
# shap_values.shape: (94, 24)  → 94ラップ × 24変数

base_val = float(np.atleast_1d(explainer.expected_value)[0])
print(f"ベース値: {base_val:.4f}")
# → ベース値: 12.3258秒

# 平均|SHAP|で重要度を確認
mean_abs_shap = np.abs(shap_values).mean(axis=0)

shap_values は (94, 24) の行列で、各要素が「そのラップ・その変数のSHAP値(秒)」です。explainer.expected_value はshapのバージョンによってはnumpy配列になるため、float(np.atleast_1d(...)[0]) で取り出します。

図1:SHAP Summary Plot(全体像)

01_shap_summary.png

Beeswarm Plot(ビースウォームプロット)は、データ点が重なるのを避けるため横方向にずらして配置する散布図で、点の群れが蜂の群れに見えることが名前の由来です。全94ラップ × 24変数のSHAP値を1枚に収めており、各点が1ラップを表します。

  • 横軸:SHAP値(秒)。負なら速い方向、正なら遅い方向への寄与
  • 縦軸:平均|SHAP|の降順(上の変数ほど重要)
  • 点の色:その変数の実際の値(赤=高い、青=低い)

throttle_pct__mean は赤い点(高スロットル)が左(速い方向)に、青い点(低スロットル)が右(遅い方向)に分布しており、「スロットルを多く開けるほどタイムが縮まる」という関係が明確です。lat_g__mean も幅が広いですが、それ以下の変数は影響が小さく、この2変数が支配的であることが視覚的に確認できます。

図2:SHAP 特徴量重要度(平均|SHAP|)

02_shap_importance.png

mean_abs_shap = np.abs(shap_values).mean(axis=0)
shap_imp_df = pd.DataFrame({'feature': op_cols, 'mean_abs_shap': mean_abs_shap}
                           ).sort_values('mean_abs_shap', ascending=False)
変数 平均|SHAP|(秒) 解釈
throttle_pct__mean 0.2180 T1区間の平均スロットル開度
lat_g__mean 0.1179 旋回中の横G(ライン取りの結果)
lon_g__std 0.0089 前後Gの変動(加減速の安定性)
wheel_rotation_radians__mean 0.0074 平均ステアリング角

throttle_pct__mean の平均|SHAP|=0.218秒は、このモデルにおける 平均的な寄与の大きさ です。2位の lat_g__mean(0.118秒)とも差があり、3位以下は一桁小さい。T1通過タイムの差を生む主因は「スロットル開度」と「横G」の2変数に集約されています。

なお、lat_g__mean はライン取りそのものではなく、速度・旋回半径・ステアリング操作の結果として現れる車両挙動です。ライン取りや旋回状態を反映する代表的な指標として扱っています。

図3:Waterfall Plot — 最速ラップ vs 最遅ラップ

03_shap_waterfall.png

最速ラップ(11.900秒)と最遅ラップ(13.500秒)を並べて、それぞれの予測根拠を変数ごとに分解します。棒グラフの 左向き(負)がタイム短縮への寄与右向き(正)がタイム悪化への寄与 です。

fastest_idx = np.argmin(y)
slowest_idx = np.argmax(y)

# 各ラップのSHAP値
sv_fast = shap_values[fastest_idx]  # 最速ラップのSHAP値(24次元)
sv_slow = shap_values[slowest_idx]  # 最遅ラップのSHAP値

# 予測値 = ベース値 + SHAP値の合計
pred_fast = base_val + sv_fast.sum()  # ≈ 11.935秒
pred_slow = base_val + sv_slow.sum()  # ≈ 13.236秒

最速ラップでは throttle_pct__mean のSHAP値が大きく負(速い方向)に出ており、ベース値からの差(-0.39秒)の大部分を単独で担っています。最遅ラップでは同変数が正(遅い方向)に転じており、ベース値からの乖離を拡大する方向に寄与しています。

この 同じ変数が正にも負にもなる という点が、特徴量重要度との本質的な違いです。重要度は「この変数は全体的に効く」を言いますが、SHAPは「このラップではプラス方向に効いた」という方向と量を同時に示します。

図4:Dependence Plot — スロットル開度とSHAP値の関係

04_shap_dependence_throttle.png

feat = 'throttle_pct__mean'
feat_idx = op_cols.index(feat)
x_vals  = X_op[:, feat_idx]   # 非標準化スロットル値
sv_vals = shap_values[:, feat_idx]

sc = ax.scatter(x_vals, sv_vals, c=y, cmap='RdYlGn_r', alpha=0.7)
  • 横軸:T1区間の平均スロットル開度
  • 縦軸:throttle_pct__mean の SHAP値(秒)
  • 点の色:実際のT1タイム(赤=遅い、緑=速い)

右下がりのパターンが明確です。スロットル開度が高いほどSHAP値が負(速い方向)になり、低スロットルではSHAP値が正(遅い方向)になります。点の色(タイム)も右下が緑(速い)、左上が赤(遅い)となっており、「スロットル開度 → SHAP値 → タイム」の関係が一貫して見えます。

ただし、これは「無条件にスロットルを多く開ければ速くなる」という因果を示すものではありません。高いスロットル開度を維持できるライン・姿勢・速度条件が整っているラップほど速い、という解釈が自然です。

まとめ

ポイント 内容
ベース値 12.326秒(全94ラップの平均タイムに相当)
最重要変数 throttle_pct__mean(平均|SHAP|=0.218秒)
2番手 lat_g__mean(平均|SHAP|=0.118秒)
個別ラップの分解 最速ラップはthrottleが-0.4秒以上の貢献、最遅ラップは+0.3秒以上の阻害
特徴量重要度との違い 方向と量を個別サンプルごとに説明できる

SHAP値は「ブラックボックスモデルの予測根拠を人間が読める形に分解する」ツールです。モデルが高精度であっても、予測の理由を説明できなければ実務での活用は限られます。SHAPはその橋渡しをします。

今回の結果は、Random ForestとLassoという異なる手法で得た「スロットル開度と横Gが支配的」という結論を、より解釈可能な形で補強しています。手法が違えど同じ変数が残ることは、その変数の重要性を裏付けます。

関連記事

0
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?