Kaggleカロリー予測で実践！XGBoost × Optuna × SHAPによる回帰モデル構築

## SHAPによる特徴量の可視化

XGBoostやLightGBMのような勾配ブースティングモデルは、SHAPを使って、予測に対する各特徴量の影響度を定量的に評価しました。

今回はLightGBMでSHAP値を計算し、最も影響の大きい特徴量Top20を抽出しました。  
その中から、精度と解釈性のバランスを見て特徴量を選定しています。

```python
shpa_data = train_df.sample(1000, random_state=42)
X_df = shpa_data.drop(['id', 'Sex', 'Calories_log', 'Calories'], axis=1)
y_df = shpa_data["Calories_log"]

model = LGBMRegressor(random_state=42)
model.fit(X_df, y_df)

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_df)
shap.summary_plot(shap_values, X_df, plot_type="bar")
```

selected_featureにある変数の組み合わせは、一番スコアが高いものになります。
```python
selected_features = [
'Duration_mul_Heart_Rate', 
'Heart_Rate_mul_Body_Temp',
 'Age_mul_Duration',
 'Age_mul_Heart_Rate',
 'Duration_mul_Body_Temp',
 'Age_mul_Weight',
 'Height_mul_Duration',
 'Weight_mul_Body_Temp',
 'Weight_mul_Heart_Rate',
 'Age_mul_Height',
 'Weight_mul_Duration',
 'Heart_Rate',
 'Height_mul_Heart_Rate',
 'Weight',
 'Height_mul_Body_Temp', 
 'Weight_mul_Height',
 'BMI',
 'Body_Temp',
 'Age',
 'Height',
 'Age_mul_Body_Temp',
 'Duration',
'Sex'
]


# 特徴量と目的変数
train_df["Sex"] = train_df["Sex"].astype("category")
test_df["Sex"] = test_df["Sex"].astype("category")

X = train_df[selected_features]
Y = train_df["Calories_log"].values

# テストデータの整形（カテゴリがある場合は型変換も）
test_X = test_df[selected_features]test_X = test_df[selected_features]
```



# 2.Optunaによる自動ハイパーパラメータチューニング
各パラメータの役割（抜粋）
n_estimators：木の本数（多いほど精度上がるが学習時間も増える）
max_depth：木の深さ（深いほど複雑な関係も学習できる）
learning_rate：学習率（小さいと学習が安定するが時間がかかる）
subsample：データのサブサンプル比率（過学習を防ぐ）
colsample_bytree：特徴量のサブサンプル比率
reg_alpha, reg_lambda：L1・L2正則化（モデルの複雑さを制御）tree_method='gpu_hist'：GPUで高速学習



``` python 
def bayes_objective(trial):
    params = {
        'n_estimators': trial.suggest_int("n_estimators", 500, 1000),
        'max_depth': trial.suggest_int("max_depth", 3, 12),
        'learning_rate': trial.suggest_float("learning_rate", 0.01, 0.1,log=True),
        'subsample': trial.suggest_float("subsample", 0.5, 1.0),
        'colsample_bytree': trial.suggest_float("colsample_bytree", 0.5, 1.0),
        "max_bin": trial.suggest_int("max_bin", 256, 1024, step=128),
        'gamma': trial.suggest_float("gamma", 0, 5),
        'objective': 'reg:squarederror',
        'eval_metric': 'rmse',    
       'reg_alpha': trial.suggest_float("reg_alpha", 0, 1),
       'reg_lambda': trial.suggest_float("reg_lambda", 1e-5, 1.0, log=True),  
        'tree_method': 'gpu_hist',
        "predictor": "gpu_predictor",
        'verbosity': 0,
        'seed': 42

                }

    # KFoldにより、データを5分割して交差検証
    kf = KFold(n_splits=5, shuffle=True, random_state=42)
    rmse_scores = []

    for train_idx, val_idx in kf.split(X):
        X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx]
        y_tr, y_val = Y[train_idx], Y[val_idx]

        dtrain = xgb.DMatrix(X_tr, label=y_tr, enable_categorical=True)
        dval = xgb.DMatrix(X_val, label=y_val, enable_categorical=True)

        evals = [(dval, 'eval')]
        model = xgb.train(
            params,
            dtrain,
            num_boost_round=10000,
            evals=evals,
            early_stopping_rounds=100,
            verbose_eval=False
        )

        preds = model.predict(dval, iteration_range=(0, model.best_iteration + 1))
        rmse = np.sqrt(mean_squared_error(np.exp(y_val), np.exp(preds))) #対数から戻して評価
        rmse_scores.append(rmse)

    return -np.mean(rmse_scores)

# Optuna でチューニング
study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=42))
study.optimize(bayes_objective, n_trials=100)
print(f"成功した試行数: {len([t for t in study.trials if t.state.name == 'COMPLETE'])}")


# 最適パラメータ
best_params = study.best_trial.params
best_score = study.best_trial.value
print(f"最適パラメータ: {best_params}\nスコア: {best_score}")

```


# 3. 最適なパラメータでXGBoostを学習・予測
```python
final_params = {
    **best_params,
    'n_estimators': 10000,        # 最大木数（EarlyStoppingで打ち止めされる）
    'random_state': 42,
    'tree_method': 'gpu_hist',    # GPUで高速学習
    'verbosity': 0
}

# KFold で最終予測（テストデータ）
kf = KFold(n_splits=5, shuffle=True, random_state=42)
test_pred_log_submit = np.zeros(len(test_X))
oof_preds = np.zeros(len(X))


for fold, (train_idx, val_idx) in enumerate(kf.split(X)):
    print(f"Fold {fold + 1}")
    X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx]
    y_tr, y_val = Y[train_idx], Y[val_idx]

    # callbacksをコンストラクタで渡す（推奨）
    model = XGBRegressor(
        **final_params,
        callbacks=[EarlyStopping(rounds=100, save_best=True)],
        enable_categorical=True  #Sexがカテゴリタイプ変数だから
    )

    model.fit(
        X_tr, y_tr,
        eval_set=[(X_val, y_val)],
        verbose=False
    )

    best_iteration = model.get_booster().best_iteration
    test_pred_log_submit += model.predict(test_X, iteration_range=(0, best_iteration)) / kf.get_n_splits()
    oof_preds[val_idx] = model.predict(X_val, iteration_range=(0, best_iteration))

```
このようにして、Optunaで得た最適パラメータを使いながら、XGBoostをKFoldで学習 → テストデータへの予測を平均することで、精度が高く安定したモデルを構築できました。

次のステップでは、logスケールを元に戻し、RMSEやR²スコアを算出してモデルの評価を行います。



# 4. 評価
Out-of-Foldの予測結果を使って、モデルの性能を2つのスケールで評価しました。

- 実スケール（logを戻した値）でのRMSE → 現実的な誤差感覚をつかむ
- logスケールでのRMSE＆R² → 学習中の最適化対象との整合性を確認
```python
# 評価指標
rmse = mean_squared_error(np.exp(Y), np.exp(oof_preds), squared=False)
r2 = r2_score(Y, oof_preds)
print(f"✅ KFoldによるCVスコア\nRMSE: {rmse:.4f}\nR²  : {r2:.4f}")

rmse = mean_squared_error(Y, oof_preds, squared=False)
r2 = r2_score(Y, oof_preds)
print(f"✅ KFoldによるCVスコア\nRMSE: {rmse:.4f}\nR²  : {r2:.4f}")
```


モデルの数値評価に加えて、予測値と実測値の散布図を描画することで、精度を視覚的に確認しました。
```python
plt.figure(figsize=(8, 6))
plt.scatter(np.expm1(Y), np.expm1(oof_preds), alpha=0.5)
plt.plot([0, max(np.expm1(Y))], [0, max(np.expm1(Y))], color="red", linestyle="--", label="完全一致ライン")
plt.xlabel("実際の値（正解）", fontsize=12)
plt.ylabel("予測値", fontsize=12)
plt.title("KFold予測値 vs 実測値（XGBoost）", fontsize=14)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

```