今回は、Kaggle Playground Series – “Predicting Road Accident Risk“に参加し、LightGBM・CatBoost・XGBoostを組み合わせたスタッキング回帰モデルを作成しました。
最終的には RidgeCVをメタモデルとして採用し、さらに 平均値・標準偏差・モデル間の掛け合わせ特徴 を導入することで、スコアを大きく改善することができました。
本記事では、そのアプローチと工夫の詳細を解説します。
Kaggleコンペティションの概要
このコンペは、道路事故の発生リスクを予測する回帰タスクです。
各観測データには「道路の種類」「制限速度」「車線数」「天候」「時間帯」などの特徴量が与えられており、それらから accident_risk(事故リスクスコア) を予測します。
モデルの汎化性能や特徴量設計の工夫がスコアに直結する構成になっています。
Notebook
実際にKaggleのコンペティション(Predicting Road Accident Risk)に参加して作成したNotebookをこちらで公開しています。このNotebookにあるコードについての解説をメインに行います。
実行結果
コンペで残したコード実行結果(抜粋)とコンペ結果(2025/10/25現在)を記します。
===============================
Final Stacking RMSE (RidgeCV + mean/std): 0.05600
Best alpha: 0.01
===============================RMSE(Root Mean Squared Error)は 0.05600 を達成。
単一モデルよりも安定した汎化性能が得られています。
Public Scoreは0.05553と好成績を記録。LeaderboardのPublicで上位20%内に入っています。
使用データ
データはKaggle上で配布されている”Predicting Road Accident Risk”のデータセットです。
モデル構成
| 層 | モデル | 役割 |
|---|---|---|
| 一次層 | LightGBM | 高速・高精度なGBDT |
| 一次層 | CatBoost | カテゴリ変数に強いGBDT |
| 一次層 | XGBoost | 安定した性能のGBDT |
| 二次層 | RidgeCV | モデル出力の統合・正則化 |
スタッキングとは?
スタッキング(Stacking)とは、複数のモデルの出力を組み合わせて、より高精度な予測を行う方法です。
1段目(一次層)では、複数のモデルが同じデータを使って予測を行い、
2段目(二次層)では、それらの予測結果を新しい特徴量として扱い、
さらに別のモデルで最終的な予測をします。
使用ライブラリ
import pandas as pd
import numpy as np
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import LabelEncoder
import lightgbm as lgb
from catboost import CatBoostRegressor
from xgboost import XGBRegressor
from sklearn.linear_model import RidgeCV
import itertoolsここで使う主なライブラリの役割は以下の通りです:
| ライブラリ | 役割 |
| pandas / numpy | データ処理 |
| scikit-learn | 交差検証・評価・線形回帰 |
| LightGBM / CatBoost / XGBoost | 高性能な勾配ブースティングモデル |
| itertools | 特徴量の組み合わせ生成 |
実装ステップ
1. データの読み込み
# --- Load data ---
train = pd.read_csv("/kaggle/input/playground-series-s5e10/train.csv")
test = pd.read_csv("/kaggle/input/playground-series-s5e10/test.csv")
target = "accident_risk"
bool_cols = ["road_signs_present", "public_road", "holiday", "school_season"]
cat_cols = ["road_type", "lighting", "weather", "time_of_day"]Kaggleのtrain.csvとtest.csvを読み込み、
目的変数(accident_risk)とブール変数、カテゴリ変数を定義します。
2.ブール変数とカテゴリ変数の数値化
# Convert bool column to 0/1
for col in bool_cols:
train[col] = train[col].astype(int)
test[col] = test[col].astype(int)
# String category string only LabelEncoder
for col in cat_cols:
le = LabelEncoder()
all_values = pd.concat([train[col], test[col]], axis=0).astype(str)
le.fit(all_values)
train[col] = le.transform(train[col].astype(str))
test[col] = le.transform(test[col].astype(str))LabelEncoder によって文字カテゴリを数値に変換します。
全データ(train + test)を合わせてfitすることで、未知カテゴリの発生を防いでいます。
3.特徴量エンジニアリング
# --- Basic feature engineering ---
for df in [train, test]:
df["speed_curvature"] = df["speed_limit"] * df["curvature"]
df["lane_density"] = df["num_lanes"] / (df["speed_limit"] + 1)
df["accident_per_lane"] = df["num_reported_accidents"] / (df["num_lanes"] + 1)既存の情報から新しい特徴を作ります。
- speed_curvature:速度とカーブのかけ合わせ(高速×急カーブ=危険)
- lane_density:車線密度
- accident_per_lane:1車線あたりの事故発生率
分母に+1を加えて箇所があるのは、0で割るのを防ぐためのテクニックです。
4.各モデルのパラメータ設定
# --- Define base models ---
lgb_params = dict(
objective="regression",
metric="rmse",
learning_rate=0.02,
num_leaves=150,
feature_fraction=0.8,
bagging_fraction=0.8,
random_state=42,
n_estimators=3000
)
cb_params = dict(
iterations=3000,
learning_rate=0.03,
depth=8,
loss_function="RMSE",
eval_metric="RMSE",
random_seed=42,
verbose=False
)
xgb_params = dict(
objective="reg:squarederror",
eval_metric="rmse",
learning_rate=0.03,
max_depth=8,
subsample=0.8,
colsample_bytree=0.8,
n_estimators=3000,
random_state=42
)ここでモデルの「性格」を決めます。学習率を小さく設定し、安定性を重視しています。
はじめのlgb_paramsはLightGBM(Light Gradient Boosting Machine)モデルの設定(ハイパーパラメータ)を指定しています。LightGBMは、Microsoft Researchによって開発された機械学習アルゴリズムで、勾配ブースティング決定木(GBDT)をベースとしています。このアルゴリズムは特に大規模データセットや高精度が求められるタスクで優れた性能を発揮します。
各パラメータ詳細
| パラメータ名 | 意味 | 説明 |
|---|---|---|
objective="regression" | 目的関数 | 回帰問題(数値予測)を解く設定。分類の場合は "binary" などになる。 |
metric="rmse" | 評価指標 | RMSE(平均二乗誤差の平方根)を使ってモデルの性能を評価する。 |
learning_rate=0.02 | 学習率 | 1回の学習ステップでどれくらい重みを更新するか。小さいほどゆっくり学習するが精度が安定。 |
num_leaves=150 | 葉の数 | 1本の決定木が持てる最大の「枝(終端)」の数。大きいほど複雑な木になり、過学習しやすい。 |
feature_fraction=0.8 | 特徴量サンプリング率 | 各木を作るときに80%の特徴量だけをランダムに使う(過学習防止)。 |
bagging_fraction=0.8 | データサンプリング率 | 各木の学習に使うデータを80%だけランダムに選ぶ(過学習防止)。 |
random_state=42 | 乱数シード | 実験の再現性を保つための固定乱数シード。 |
n_estimators=3000 | 木の本数 | 3000本の決定木を作る。早期終了(early stopping)で途中停止するので多めに設定している。 |
2つ目のcb_paramsはCatBoostモデルの設定(ハイパーパラメータ)を指定しています。CatBoostは勾配ブースティングアルゴリズムの一種で、特にカテゴリカル変数(カテゴリデータ)の処理に優れている点が特徴です。
各パラメータ詳細
| パラメータ名 | 意味 | 説明 |
|---|---|---|
iterations=3000 | 学習の繰り返し回数(木の本数) | 最大で3000本の決定木を作る。ただし早期終了(early stopping)で途中停止することもある。 |
learning_rate=0.03 | 学習率 | 一度にどれくらい重みを更新するか。小さいほど安定だが時間がかかる。 |
depth=8 | 木の深さ | 1本あたりの決定木の深さ。値が大きいほど複雑になり、過学習のリスクが上がる。 |
loss_function="RMSE" | 損失関数 | モデルが学習中に最小化しようとする指標。回帰問題なのでRMSE(平均二乗誤差の平方根)を使用。 |
eval_metric="RMSE" | 評価指標 | 検証データで性能を測る指標。これもRMSE。 |
random_seed=42 | 乱数シード | 結果を再現できるように乱数を固定。 |
verbose=False | 出力抑制 | 学習中のログを非表示にする設定。Trueにすると各ステップのRMSEが出力される。 |
3つ目のxgb_paramsはXGBoost(eXtreme Gradient Boosting)モデルの設定(ハイパーパラメータ)を指定しています。XGBoostは機械学習におけるアンサンブル学習の一種で、特に勾配ブースティングアルゴリズムを用いた強力なモデルです。このアルゴリズムは、決定木を基盤とし、複数の弱学習器を直列的に結合することで、予測精度を向上させます。
各パラメータ詳細
| パラメータ名 | 意味 | 説明 |
|---|---|---|
objective="reg:squarederror" | 目的関数 | 回帰問題用の設定。予測値と実際の値の「二乗誤差(平方誤差)」を最小化する。分類の場合は "binary:logistic" などを使用。 |
eval_metric="rmse" | 評価指標 | 検証データで性能を測る際の指標。RMSE(平均二乗誤差の平方根)を使用して予測精度を評価する。 |
learning_rate=0.03 | 学習率 | 1本の木を追加するときの影響度。小さいほど安定するが学習に時間がかかる。一般的に 0.01〜0.1 の範囲で調整する。 |
max_depth=8 | 木の深さ | 各決定木の最大の深さ。大きいほど複雑なモデルになり、過学習のリスクが高まる。一般的には 4〜10 程度で調整。 |
subsample=0.8 | データサンプリング率 | 各木を作るときに使用する訓練データの割合。過学習防止のために 0.5〜1.0 の間で設定する。 |
colsample_bytree=0.8 | 特徴量サンプリング率 | 各木を作るときに使用する特徴量の割合。過学習を防ぐ目的で 0.5〜1.0 の範囲で指定する。 |
n_estimators=3000 | 木の本数 | 最大で作成する木の数。早期終了(early stopping)を使うことで、最適な手前で自動的に止まる。多めに設定しておくのが一般的。 |
random_state=42 | 乱数シード | ランダム性を固定し、同じ結果を再現できるようにする。どんな値でも良いが、慣習的に「42」がよく使われる。 |
5.KFoldによる交差検証
# --- 5-Fold Stacking ---
kf = KFold(n_splits=5, shuffle=True, random_state=42)
features = [col for col in train.columns if col not in ["id", target]]
oof_lgb, oof_cb, oof_xgb = np.zeros(len(train)), np.zeros(len(train)), np.zeros(len(train))
test_lgb, test_cb, test_xgb = np.zeros(len(test)), np.zeros(len(test)), np.zeros(len(test))5分割交差検証を行い、各foldごとにLightGBM / CatBoost / XGBoostを学習してOOF(Out-Of-Fold)予測を蓄積していきます。
| パラメータ | 意味 | 具体的な動作 |
|---|---|---|
n_splits=5 | データを5分割する | データを5つのブロックに分け、1つを検証用・残り4つを学習用として5回繰り返す。 |
shuffle=True | データをランダムに並び替える | 分割前にランダムシャッフルして、偏りのない分割を行う。 |
random_state=42 | 乱数の固定(再現性確保) | 毎回同じシャッフル結果になるようにする。 |
for fold, (tr_idx, val_idx) in enumerate(kf.split(train)):
print(f"===== Fold {fold+1} =====")
X_train, X_valid = train.iloc[tr_idx][features], train.iloc[val_idx][features]
y_train, y_valid = train.iloc[tr_idx][target], train.iloc[val_idx][target]
# LightGBM
model_lgb = lgb.LGBMRegressor(**lgb_params)
model_lgb.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], eval_metric="rmse",
callbacks=[lgb.early_stopping(100), lgb.log_evaluation(0)])
oof_lgb[val_idx] = model_lgb.predict(X_valid)
test_lgb += model_lgb.predict(test[features]) / kf.n_splits
# CatBoost
model_cb = CatBoostRegressor(**cb_params)
model_cb.fit(X_train, y_train, eval_set=(X_valid, y_valid),
use_best_model=True, early_stopping_rounds=100)
oof_cb[val_idx] = model_cb.predict(X_valid)
test_cb += model_cb.predict(test[features]) / kf.n_splits
# XGBoost
model_xgb = XGBRegressor(**xgb_params)
model_xgb.fit(X_train, y_train, eval_set=[(X_valid, y_valid)],
early_stopping_rounds=100, verbose=False)
oof_xgb[val_idx] = model_xgb.predict(X_valid)
test_xgb += model_xgb.predict(test[features]) / kf.n_splitsこれにより、全データに対するバイアスの少ない学習が可能になります。
eval_set=[(X_valid, y_valid)]: 検証データを指定。学習中に性能を確認するeval_metric="rmse": 検証性能の評価指標(Root Mean Squared Error)lgb.early_stopping(100): 検証スコアが100ラウンド改善しなければ学習を停止lgb.log_evaluation(0): 学習中のログ出力をなしにするuse_best_model=True: 学習中の検証スコアが最良の時点のモデルを保持verbose=False: 学習ログを非表示にする
6.一次層出力の作成
# --- Create meta features ---
train_stack = pd.DataFrame({
"lgb": oof_lgb,
"cb": oof_cb,
"xgb": oof_xgb
})
test_stack = pd.DataFrame({
"lgb": test_lgb,
"cb": test_cb,
"xgb": test_xgb
})3モデルの予測値を1つのDataFrameにまとめ、これを「メタ特徴」として次の層に渡します。
7.拡張特徴量の追加(interactions + mean/std)
# Add interaction terms + mean/std among base models
def add_interactions(df):
df_new = df.copy()
for (c1, c2) in itertools.combinations(df.columns, 2):
df_new[f"{c1}_x_{c2}"] = df[c1] * df[c2]
df_new["mean"] = df.mean(axis=1)
df_new["std"] = df.std(axis=1)
return df_new
train_stack_ext = add_interactions(train_stack)
test_stack_ext = add_interactions(test_stack)生成される拡張特徴量:
lgb_x_cb,lgb_x_xgb,cb_x_xgb(モデル間の相互作用)mean,std(3モデルの平均・ばらつき)
合計で 3(元の特徴)+3(掛け合わせ)+2(平均・標準偏差)=8特徴 になります。
7.メタモデル(RidgeCV)
# --- RidgeCV as meta model ---
alphas = [1e-3, 1e-2, 0.05, 0.1, 0.3, 1.0, 3.0, 10.0]
meta_model = RidgeCV(alphas=alphas, scoring="neg_root_mean_squared_error", cv=5)
meta_model.fit(train_stack_ext, train[target])- RidgeCVはL2正則化付き線形モデル
- 自動で最適なα(正則化強度)を探索してくれます
- 本結果では「Best alpha: 0.01」となりました
- 小さい alpha → 正則化弱い(学習データに近い予測)
- 大きい alpha → 正則化強い(モデルが単純になり過学習防止)
- scoring=”neg_root_mean_squared_error” : RMSE(Root Mean Squared Error)の符号を反転させて使用
8.評価と提出
# Metamodel predictions
final_oof = meta_model.predict(train_stack_ext)
final_preds = meta_model.predict(test_stack_ext)
rmse = mean_squared_error(train[target], final_oof, squared=False)
print(f"Final Stacking RMSE (RidgeCV + mean/std): {rmse:.5f}")
# --- Submission ---
submission = pd.DataFrame({
"id": test["id"],
"accident_risk": final_preds
})
submission.to_csv("submission.csv", index=False)
print("submission.csv saved!")- RMSE(誤差指標)が小さいほど良いモデルです
- 本結果ではRMSE=0.05600となり、Kaggle上でも上位レベルの安定したスコアです。
まとめ
この構成では、以下の要素が高い精度を支えています:
- 3種類のGBDTモデルを併用したアンサンブル
- mean/stdとinteractionによる2次特徴拡張
- RidgeCVによる過学習抑制
- KFoldでの安定した評価
結果として、単一モデルを超える精度と安定性を実現しました。
