レーシングシミュレータの実データを使って、データサイエンスの概念を実装とともに解説しています。この記事では 残差分析(Residual Analysis) を扱います。
こんな人に役立ちます
- 回帰モデルの「残差」が何を意味するか実データで理解したい
- CV予測(交差検証予測)と学習データ予測の違いを理解したい
- モデルが大きく外れたサンプルをどう調べるか知りたい
結論
富士スピードウェイ1コーナー(T1区間、500〜1000m)のラップデータを使い、前記事のRandom Forest(CV R²=0.848)に対して残差分析を行いました。
- 残差の平均:+0.011秒(ほぼゼロ)、標準偏差:0.156秒
- 最大残差:+0.633秒(実測13.5秒に対して12.9秒と予測 → 0.6秒以上遅い方向に外れた)
- 正規性検定(Shapiro-Wilk):p=0.000 → 残差は正規分布でない(右テールが重い)
-
遅い方向外れラップの特徴:
throttle_pct__meanの平均が他グループより低い(48.99% vs 53.33%)。モデルが学習した「スロットル開度→タイム」の関係とは異なる何かが起きていた
残差分析は「モデルが何を説明できなかったか」を明らかにします。 SHAPが予測根拠を説明するツールなら、残差分析は予測の限界を可視化するツール です。
問い
CV R²=0.848のモデルでも、0.6秒以上外れたラップがありました。
前記事でSHAP値を使って「このラップの予測根拠」を変数ごとに分解しました。しかし、モデルの予測が大きく外れたラップについては、SHAPは根拠を「示す」だけで、その予測が正しかったかどうかは教えてくれません。
問いを変えましょう。
「モデルが正しく予測できなかったラップには、何が起きていたか」
これが残差分析の出発点です。残差(residual)とは、回帰モデル(連続値を予測するモデル)における実測値と予測値の差で、モデルが説明しきれなかった部分を指します。
$$\text{残差} = \text{実測値} - \text{予測値}$$
残差の分布を調べることで次の問いに答えられます。
- 残差はランダムか、それとも構造的なパターンがあるか
- 大きく外れたサンプルには共通の特徴があるか
- モデルの限界はどこにあるか
この手法はモータースポーツに限らず、あらゆる回帰モデルの評価に使えます。 製品の品質予測モデルが「このロットだけ大きく外れた」という状況は製造業でよく起きます。外れた原因を変数で追う手順は同じです。
| 領域 | モデルが予測するもの | 残差分析で調べること |
|---|---|---|
| モータースポーツ | セクタータイム | 大きく外れたラップの操作特性 |
| 製造業 | 製品の品質・寸法 | 外れロットのプロセスパラメータ |
| EC | 売上・CVR | 予測を大きく下回った日の要因 |
| 金融 | 株価・リスク | モデルが外れたタイミングの市場状態 |
残差分析の前提:CV予測を使う
残差分析には 交差検証予測(CV予測) を使う必要があります。学習データでの予測を使ってはいけません。
Random Forestは学習したデータを「記憶」しているため、同じデータで予測すると実態より精度が高く見えます(過学習:学習データに特化しすぎて未知データへの精度が落ちる現象)。今回のモデルでは訓練R²=0.979ですが、これは「学習済みのデータへの当てはまりの良さ」であり、未知データへの精度ではありません。
| 予測方法 | R² | 残差の標準偏差 | 問題 |
|---|---|---|---|
rf.predict(X_scaled) |
0.979 | 非常に小さい | 記憶したデータを予測しているため残差が人工的に小さい |
cross_val_predict(...) |
0.848 | 0.156秒 | 各ラップを「見たことがない状態」で予測 → 現実的な残差 |
cross_val_predict は5分割交差検証の各Foldで「そのラップを学習に使っていないモデル」で予測します。全94ラップが一度ずつテストデータとして予測されるため、実際の汎化性能に基づいた残差が得られます。
# NG:学習データでの予測(残差が人工的に小さくなる)
y_pred_train = rf.predict(X_scaled)
# OK:CV予測(各ラップをテストデータとして予測)
from sklearn.model_selection import cross_val_predict
y_pred_cv = cross_val_predict(rf, X_scaled, y, cv=kf)
residuals = y - y_pred_cv
データ準備
使用ライブラリ:pandas、numpy、scipy、scikit-learn、matplotlib
ドライバー起因の操作変数24個(throttle_pct、brake_pct、lat_g、lon_g、gear、wheel_rotation_radians の mean/max/min/std)を使います。データ準備と外れ値処理の詳細は前記事を参照してください。
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_predict, KFold
from scipy import stats
rf = RandomForestRegressor(n_estimators=300, random_state=42, n_jobs=-1)
kf = KFold(n_splits=5, shuffle=True, random_state=42)
rf.fit(X_scaled, y)
y_pred_cv = cross_val_predict(rf, X_scaled, y, cv=kf)
residuals = y - y_pred_cv
print(f"残差の平均: {residuals.mean():.4f}秒")
print(f"残差の標準偏差: {residuals.std():.4f}秒")
print(f"残差の最大: {residuals.max():.4f}秒")
print(f"残差の最小: {residuals.min():.4f}秒")
# 残差の平均: 0.0111秒
# 残差の標準偏差: 0.1560秒
# 残差の最大: 0.6334秒
# 残差の最小: -0.4898秒
図1:残差の分布
残差の平均は +0.011秒(≈0)で、系統的なバイアスはありません。しかし分布の形に注目してください。
stat, p_val = stats.shapiro(residuals)
print(f"Shapiro-Wilk: W={stat:.4f}, p={p_val:.4f}")
# → W=0.8702, p=0.0000
Shapiro-Wilk検定は「この数値の並びは正規分布から来ているか」を確かめる検定です。構造は次の通りです。
- 帰無仮説(H₀):残差は正規分布に従う
- 対立仮説(H₁):正規分布に従わない
p値は「帰無仮説が正しいと仮定したとき、今回のような(またはそれ以上に極端な)データが偶然得られる確率」です。p=0.000 はその確率がほぼゼロであることを意味し、帰無仮説を棄却 → 残差は正規分布でない と判断します。
ヒストグラムを見ると左側(速い方向)はきれいに収まっていますが、右側(遅い方向)に長いテールがあります。正規分布の「左右対称・テールが薄い」形と一致しないため、検定に引っかかっています。
残差が正規分布に近いほど「モデルが説明できなかった部分がランダムなノイズ」に近く、特定のパターンが残っていないことを示します。右テールが重いということは、モデルが捉えきれない「遅い方向の外れ要因」が存在することを示唆します。
図2:実測値 vs CV予測値
各ラップを点としてプロットし、残差の大きさで色付けしています(赤=遅い方向外れ、緑=速い方向外れ)。対角線(残差=0)から大きく離れた赤い点が「モデルが大きく外れたラップ」です。
最も外れた3ラップ:
| idx | 実測(秒) | CV予測(秒) | 残差(秒) | throttle_pct__mean | lat_g__mean |
|---|---|---|---|---|---|
| 29 | 13.500 | 12.867 | +0.633 | 46.2% | -0.623 |
| 25 | 13.450 | 12.837 | +0.613 | 50.0% | -0.603 |
| 46 | 13.083 | 12.637 | +0.446 | 50.9% | -0.456 |
idx=29のラップはモデルが「12.9秒」と予測したにもかかわらず実際は「13.5秒」かかっています。throttle_pct__mean=46.2%と他のラップより低いですが、それだけでは +0.633秒の乖離を説明できません。
なお、スピンや明確なコースアウトといった異常ラップは事前の外れ値処理で除外済みです。これらは 「普通のラップに見えるのに、モデルが大きく外れた」 ケースです。
図3:残差 vs 予測値(構造的なバイアスの確認)
横軸を予測値、縦軸を残差にしたプロットです。残差に 構造的なパターン(例:予測値が大きいほど残差も大きいなど)がなければ、モデルに系統的な問題はないと判断できます。
今回の結果では、残差は予測値の全域にほぼランダムに分布しており、系統的なバイアスは見られません。ただし右テール(大きな正残差)が数点存在しており、特定の状況下でモデルが対応できていないことが読み取れます。
図4:大外れラップの特徴量比較
残差の大きさで3グループに分けて、主要変数の分布を比較します。
n_group = 10
top_idx = np.argsort(residuals)[-n_group:] # 遅い方向外れ
bottom_idx = np.argsort(residuals)[:n_group] # 速い方向外れ
mid_idx = np.argsort(np.abs(residuals))[:n_group] # よく当たった
| グループ | 残差の平均 | throttle_pct__mean | lat_g__mean |
|---|---|---|---|
| 遅い方向外れ(上位10) | +0.339秒 | 48.99% | -0.563 |
| よく当たった(中央10) | -0.003秒 | 53.33% | -0.661 |
| 速い方向外れ(下位10) | -0.207秒 | 51.83% | -0.669 |
「遅い方向外れ」グループの throttle_pct__mean 平均は 48.99% で、よく当たったグループ(53.33%)より低くなっています。
ここで重要なのは、残差はモデルが「低スロットルは遅い」と 既に織り込んだ上での乖離 だという点です。
モデルの予測(例): 12.9秒 ← 低スロットルを見て「このラップは遅いはずだ」と調整済み
実測: 13.5秒
残差: +0.6秒 ← 低スロットルでは説明できない「追加の遅さ」
低スロットルそのものは予測に織り込まれています。残差として現れているのは、それでも説明できなかった部分です。
この追加の遅さの原因として考えられるのは、このモデルの特徴量がすべて T1区間全体の集約統計(mean/max/min/std) であることです。ラップ内の時系列の順序やパターンは捨てられています。たとえば throttle_pct__mean=48% の2本のラップが「T1序盤は低く終盤に大きく開ける」と「終始中途半端に開け続ける」では実際のタイムが異なりますが、mean だけでは区別できません。また pos_x/pos_y/pos_z(走行ライン)も除外しているため、T1を大回りしたのか小回りしたのかも特徴量に含まれていません。
一つの仮説は「アクセルオン位置(T1区間のどの距離地点でアクセルを踏み始めたか)」という情報の欠如です。T1コーナー出口で早くアクセルを踏めたラップと、踏み遅れたラップは、mean値が同じでも時間が異なります。この情報は現在の特徴量では表現できていません。
まとめ
| ポイント | 内容 |
|---|---|
| 残差の平均 | +0.011秒(系統バイアスなし) |
| 残差の標準偏差 | 0.156秒 |
| 正規性 | Shapiro-Wilk p=0.000 → 正規分布でない(右テールが重い) |
| 最大外れ | +0.633秒(変数で説明できなかった遅さ) |
| 外れラップの特徴 | throttle_pct__meanが低いにも関わらず実測がさらに遅い |
残差分析は「モデルが説明できなかった部分」を可視化します。残差がランダムであれば良いモデルの証拠ですが、外れラップに共通のパターンがある場合は、変数に含まれていない要因(特徴量として取り込んでいない情報)が存在することを示します。
今回の結果では、SHAPで示した「スロットル開度とライン取りが支配的」という構造に加えて、少数のラップでこの構造に収まらない要因が働いていたことが残差から読み取れます。
関連記事
- 「このラップはなぜ遅い?」を秒単位で分解する — SHAPで予測根拠を可視化 — 本記事の前段:予測根拠の分解
- T1の速さは「スロットルとライン」で説明できるのか — ドライバー起因変数24個でCV R²=0.848を検証 — 本記事のモデルを構築した記事
- 線形回帰はまだ強い — Lassoで「タイムに効く変数」を9個に絞る — 同データで正則化線形モデルを比較
- 時系列データから特徴量を作る — 5,965行を1行に集約する特徴量エンジニアリング — 本記事の特徴量作成
- 富士スピードウェイの1コーナーを、データサイエンスで分析する — T1区間の解説(note)



