Kaggleのカロリー予測にCatBoostで挑戦！Optuna × SHAP

KaggleのPlaygroundシリーズを題材に、CatBoostを使ってカロリー消費量の回帰予測に挑戦しました。

本記事では、以下のような実践的アプローチを組み合わせて、モデルの精度と解釈性を高めることを目指しました：

- SHAPを使った特徴量選定
- OptunaによるCatBoostの自動ハイパーパラメータ最適化
- クロスバリデーション（KFold）による安定した精度評価

# 1.特徴量選択（SHAP）
特徴量選定にはSHAPを活用し、LightGBMで計算した重要度から上位変数を抽出しました。

```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)
    
# SHAPの初期化
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_df)
    
# SHAPの可視化
#横軸が広く広がっているものはSHAP値の振れ幅が大きく目的変数に対する寄与度も高い
#重要な特徴量トップ 20がデフォルト出力
shap.summary_plot(shap_values, X_df)
    
shap.summary_plot(shap_values, X_df, plot_type="bar")
    
mean_abs_shap = np.abs(shap_values).mean(axis=0)
   
# 重要度の高い順にソート（インデックス取得）
top_indices = np.argsort(mean_abs_shap)[::-1][:20]
 
# 対応する変数名を取り出してリスト化
top_features = X_df.columns[top_indices].tolist()
    
print("トップ20の重要な特徴量リスト:")
print(top_features)
```


以下が最終的に使用した特徴量です
```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',
 'Height',
 'Age_mul_Body_Temp',
'Sex',
    ]
```


# 2. Optunaによるハイパーパラメータ最適化
``` python
    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]
    
    # Optuna 目的関数
    def catboost_objective(trial):
        params = {
            'iterations': trial.suggest_int("iterations", 1000, 3000),
            'learning_rate': trial.suggest_float("learning_rate", 0.01, 0.2, log=True),
            'depth': trial.suggest_int("depth", 4, 10),
            'l2_leaf_reg': trial.suggest_float("l2_leaf_reg", 1, 10.0),
            # 'random_strength': trial.suggest_float("random_strength", 1.0, 50.0),
            'bagging_temperature': trial.suggest_float("bagging_temperature", 0.0, 1.0),
            'loss_function': 'RMSE',
            'eval_metric': 'RMSE',
            'task_type': 'GPU',
            'verbose': False
        }
    
        kf = KFold(n_splits=50, 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]
    
            model = CatBoostRegressor(**params)
            model.fit(X_tr, y_tr, cat_features=["Sex"], eval_set=(X_val, y_val), early_stopping_rounds=100)
            preds = model.predict(X_val)
            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(catboost_objective, n_trials=100)
    
    print(f"成功した試行数: {len([t for t in study.trials if t.state.name == 'COMPLETE'])}")
    best_params = study.best_trial.params
    print(f"最適パラメータ: {best_params}\nスコア: {study.best_trial.value}")

```

# 3. KFoldでの最終モデル学習と予測

最適化されたパラメータを使って、再度KFold（50分割）で学習を行い、Out-of-Fold予測とテストデータへの最終予測を実施しました。

```python
   final_params = {
        **best_params,
        'random_seed': 42,
        'task_type': 'GPU',
        'loss_function': 'RMSE',
        'verbose': False
    }
    
    # KFoldで最終予測
    kf = KFold(n_splits=50, 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]
    
        model = CatBoostRegressor(**final_params)
        model.fit(X_tr, y_tr, cat_features=["Sex"], eval_set=(X_val, y_val), early_stopping_rounds=100)
    
        oof_preds[val_idx] = model.predict(X_val)
        test_pred_log_submit += model.predict(test_X) / kf.get_n_splits()test_pred_log_submit += model.predict(test_X) / kf.get_n_splits()
```

# 4. 評価（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}")
```

# 5. 予測と実測の可視化
``` 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 実測値（CatBoost）", fontsize=14)
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()plt.show()plt.show()plt.show()さんplt.show()plt.show()plt.show()さん
```