🎄

LightGBM vs XGBoostを比較してみた

に公開

📌はじめに

以前LightGBMって本当に強い?決定木と比べて検証してみたを投稿しましたが、
やっぱり気になるのは LightGBM vs XGBoost ですよね。

「結局どっちが強いの?」
「精度に差はあるの?」

そこで今回は、LightGBM vs XGBoost を同じ条件で比較し、
その違いを確かめてみたいと思います。

❓LightGBM vs XGBoost の違いって❓

📌共通点

  • ブースティング木モデル(弱学習器=決定木を多数積み重ねる)
  • 回帰・分類・ランキングなど幅広いタスクに対応
  • 欠損値処理や正則化を組み込み
  • GPU対応あり

📌主な違い

1. 木の成長方式

  • XGBoost:深さ優先で木を伸ばす(level-wise growth)
    • 同じ深さのノードを一気に分割
    • バランスの良い木を作りやすい
    • 計算量は増えるが、安定性が高い
  • LightGBM:リーフごとに分割を進める(leaf-wise growth)
    • 損失減少が最大になる葉を優先して分割
    • 精度が高くなりやすい
    • ただし過学習しやすい(深い木になりがち)

2. 特徴量の扱い

  • XGBoost:全特徴量を対象に分割候補を探索
  • LightGBM
    • Gradient-based One-Side Sampling (GOSS) → 勾配の大きいサンプルを優先的に使う
    • Exclusive Feature Bundling (EFB) → 疎な特徴量をまとめて効率化

👉 高次元・大規模データに強いのは LightGBM。


3. メモリ・速度

  • LightGBM:メモリ効率が高く、学習が速い(大規模データ向き)
  • XGBoost:少し重めだが、安定性とチューニングのしやすさが強み

4. ハイパーパラメータの傾向

  • XGBoost:やや直感的(max_depth, eta など)
  • LightGBM:パラメータが独特で学習コントロールに慣れが必要(num_leaves, max_depth の関係など)

✅まとめ

  • データが大きい/特徴量が多い/速度重視 → LightGBM
  • 安定性/細かい制御/チューニングしやすさ重視 → XGBoost

📌サンプルデータ:sample_car_data.csv

機械学習モデルの予測根拠を可視化:LightGBM × SHAP入門でも使用した

sample_car_data.csv(N = 400)


📌LightGBMデータ:predictions_with_top3.csv

XGBoostとの比較に使用 
機械学習モデルの予測根拠を可視化:LightGBM × SHAP入門で作成した


predictions_with_top3.csv


📌フォルダ構成

├─ 1_flow/
│   └─ shap_analysis_xgboost.py     # 実行スクリプト
├─ 2_data/
│   │─ sample_car_data.csv          # サンプルデータ
│   └─ predictions_with_top3.csv    # XGBoostとの比較に使用するデータ
├─ 3_output/                        # 出力用(自動作成)

📌環境

python3.x


📌コード(shap_analysis_xgboost.py)

コード:ライブラリ~sample_car_data.csv読み込み
#============================================
# ライブラリと変数設定
#============================================
import os
import shap
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, f1_score, ConfusionMatrixDisplay
from sklearn.tree import DecisionTreeClassifier, plot_tree
import lightgbm as lgb
import japanize_matplotlib
from xgboost import plot_importance
from xgboost import XGBClassifier, plot_tree as xgb_plot_tree
import xgboost as xgb


INPUT_FOLDER = '2_data'
INPUT_FILE = 'sample_car_data.csv'
INPUT_TOP3_FILE = "predictions_with_top3.csv"
OUTPUT_FOLDER = '3_output'

ID = 'customer_id'
target_col = 'manufacturer'
numeric_cols = ["family", "age", "children", "income"]

# 結合時にリネームする
re_name_be = "predicted_manufacturer"
re_name_af =  "predicted_class"

# =========================
# ディレクトリ・ファイル設定
# =========================
parent_path = os.path.dirname(os.getcwd())
input_path = os.path.join(parent_path, INPUT_FOLDER, INPUT_FILE)
input_top3_path = os.path.join(parent_path, INPUT_FOLDER, INPUT_TOP3_FILE)
output_path = os.path.join(parent_path, OUTPUT_FOLDER)
os.makedirs(output_path, exist_ok=True)

save_tree3models_path = os.path.join(output_path, "Confusion_Matrix_3models.png")
save_Feature_path  = os.path.join(output_path, "比較_LightGBM_XGBoost_Feature.png")
save_path  = os.path.join(output_path, "predictions_with_top3_XGBoost.csv")
save_name_path  = os.path.join(output_path, "メーカー別_SHAP_特徴ランキングXGBoost.csv")


# =========================
# CSV読み込み
# =========================
try:
    df = pd.read_csv(input_path, encoding="utf-8")
except UnicodeDecodeError:
    df = pd.read_csv(input_path, encoding="cp932")

コード:カテゴリ列自動判定~混同行列
# =========================
# カテゴリ列自動判定
# =========================
exclude_cols = [ID, target_col]
categorical_cols = [col for col in df.columns if col not in exclude_cols + numeric_cols]
df[categorical_cols] = df[categorical_cols].astype("category")

# =========================
# 説明変数と目的変数
# =========================
X_df = df.drop([ID, target_col], axis=1)
y_df = df[target_col]

# クラス数を確認
classes = np.unique(y_df)
print("クラス:", classes)

# =========================
# ラベルを0始まりに変換(LightGBM対応)
# =========================
le = LabelEncoder()
y_enc = le.fit_transform(y_df)
class_names = [str(c) for c in le.classes_]

print("クラス:", class_names)

# =========================
# データ分割(test_size=0.25)
# =========================
X_train, X_test, y_train, y_test = train_test_split(
    X_df, y_enc, test_size=0.25, random_state=0, stratify=y_enc
)
print("訓練データ数:", len(X_train))
print("テストデータ数:", len(X_test))


# ======================================
# LightGBMモデル(訓練データのみで学習)
# ======================================
objective = 'binary' if len(class_names) == 2 else 'multiclass'
metric = 'binary_error' if objective == 'binary' else 'multi_error'
params = {'objective': objective, 'metric': metric, 'verbose': -1}
if objective == 'multiclass':
    params['num_class'] = len(class_names)

lgb_train = lgb.Dataset(X_train, label=y_train, feature_name=X_df.columns.tolist())
lgb_model = lgb.train(params, lgb_train, num_boost_round=50)
# 予測
y_pred_lgb_prob = lgb_model.predict(X_test)
# y_pred_lgb = (y_pred_lgb_prob > 0.5).astype(int) if objective=='binary' else np.argmax(y_pred_lgb_prob, axis=1)
if objective == 'binary':
    y_pred_lgb = (y_pred_lgb_prob > 0.5).astype(int)
else:
    y_pred_lgb = np.argmax(y_pred_lgb_prob, axis=1)

# ==================================
# XGBoostモデル(訓練/テスト分割で学習)
# ==================================
xgb_model = XGBClassifier(
    objective='multi:softmax' if len(class_names) > 2 else 'binary:logistic',
    num_class=len(class_names) if len(class_names) > 2 else None,
    eval_metric='mlogloss' if len(class_names) > 2 else 'logloss',
    use_label_encoder=False,
    random_state=42
)

X_train_enc = X_train.copy()
X_test_enc  = X_test.copy()

for col in X_train_enc.select_dtypes(include='category'):
    X_train_enc[col] = X_train_enc[col].cat.codes
    X_test_enc[col]  = X_test_enc[col].cat.codes

xgb_model.fit(X_train_enc, y_train)
y_pred_xgb = xgb_model.predict(X_test_enc)

# =========================
# 参考指標
# =========================
print("【LightGBM】     Accuracy:", accuracy_score(y_test, y_pred_lgb))
print("【LightGBM】     F1 Score:", f1_score(y_test, y_pred_lgb, average='weighted'))

print("【XGBoost】     Accuracy:", accuracy_score(y_test, y_pred_xgb))
print("【XGBoost】     F1 Score:", f1_score(y_test, y_pred_xgb, average='weighted'))

# =========================
# 混同行列
# =========================
fig, ax = plt.subplots(1, 2, figsize=(12,5))  #LGB / XGB
ConfusionMatrixDisplay.from_predictions(y_test, y_pred_lgb, ax=ax[0])
ax[0].set_title("LightGBM")
ConfusionMatrixDisplay.from_predictions(y_test, y_pred_xgb, ax=ax[1])
ax[1].set_title("XGBoost")

plt.tight_layout()
plt.savefig(save_tree3models_path, dpi=300)
plt.show()

出力例
【LightGBM】     Accuracy: 0.73
【LightGBM】     F1 Score: 0.7273653962492439
【XGBoost】     Accuracy: 0.72
【XGBoost】     F1 Score: 0.7213349682198088

❓精度向上:モデルのチューニング❓

今回の比較では、LightGBM と XGBoost を デフォルト設定 のまま使用しました。
デフォルト設定でも十分な性能が出る場合もありますが、
モデルの性能はハイパーパラメータによって大きく変わるためチューニングはとても重要です。


主なハイパーパラメータ

  • n_estimators:学習する木の数
  • max_depth:木の深さ
  • learning_rate:学習率
  • subsample / colsample_bytree:データや特徴量のサンプリング割合
  • reg_alpha / reg_lambda:正則化パラメータ

チューニング方法

  • GridSearchCVRandomizedSearchCV を使って最適な組み合わせを探索
  • データセットの特性や目的に応じて、Top-1 / Top-3 精度や F1 スコアを評価指標に設定

コード:木~集計(平均・最大・最小)
import json
from xgboost import plot_tree as xgb_plot_tree
from xgboost import XGBClassifier, plot_tree as xgb_plot_tree

# =========================
# LightGBM(1本目の木を可視化)
# =========================
plt.figure(figsize=(20, 10))
lgb.plot_tree(lgb_model, tree_index=0, figsize=(20,10), show_info=['split_gain', 'internal_value', 'internal_count', 'leaf_count'])
plt.title("LightGBM Tree (Tree 0)")
plt.savefig(os.path.join(output_path, "LightGBM_Tree.png"), dpi=300)
plt.show()



# =========================
# XGBoost(全データで学習、可視化用)
# =========================

# 全データをコピーしてカテゴリ列を整数化
X_df_enc = X_df.copy()
for col in X_df_enc.select_dtypes(include='category'):
    X_df_enc[col] = X_df_enc[col].cat.codes

# XGBoost 全データ学習(可視化用)
xgb_model_all = XGBClassifier(
    objective='multi:softmax' if len(class_names) > 2 else 'binary:logistic',
    num_class=len(class_names) if len(class_names) > 2 else None,
    eval_metric='mlogloss' if len(class_names) > 2 else 'logloss',
    use_label_encoder=False,
    random_state=42
)
xgb_model_all.fit(X_df_enc, y_enc)

# =========================
# XGBoost(1本目の木を可視化)
# =========================

plt.figure(figsize=(20, 10))
xgb_plot_tree(xgb_model_all, num_trees=0, rankdir='LR')
plt.title("XGBoost Tree (Tree 0)")
plt.savefig(os.path.join(output_path, "XGBoost_Tree.png"), dpi=300)
plt.show()

# =========================
# XGBoost 木の深さ・ノード数
# =========================
booster = xgb_model_all.get_booster()
trees_json = booster.get_dump(dump_format='json')

def get_depth(node):
    if "children" not in node or len(node["children"]) == 0:
        return 1
    return 1 + max(get_depth(child) for child in node["children"])

def count_nodes(node):
    if "children" not in node or len(node["children"]) == 0:
        return 1
    return 1 + sum(count_nodes(child) for child in node["children"])

xgb_tree_stats = []
for i, t_str in enumerate(trees_json):
    t = json.loads(t_str)
    depth = get_depth(t)
    nodes = count_nodes(t)
    xgb_tree_stats.append({"Tree": i, "Depth": depth, "Nodes": nodes})

xgb_stats_df = pd.DataFrame(xgb_tree_stats)

# =========================
# LightGBM 木の深さ・ノード数
# =========================
lgb_json = lgb_model.dump_model()

def get_depth_lgb(node):
    if "left_child" not in node and "right_child" not in node:
        return 1
    return 1 + max(get_depth_lgb(node["left_child"]), get_depth_lgb(node["right_child"]))

def count_nodes_lgb(node):
    if "left_child" not in node and "right_child" not in node:
        return 1
    return 1 + count_nodes_lgb(node["left_child"]) + count_nodes_lgb(node["right_child"])

lgb_tree_stats = []
for t, info in enumerate(lgb_json["tree_info"]):
    tree_t = info["tree_structure"]
    depth = get_depth_lgb(tree_t)
    nodes = count_nodes_lgb(tree_t)
    lgb_tree_stats.append({"Tree": t, "Depth": depth, "Nodes": nodes})

lgb_stats_df = pd.DataFrame(lgb_tree_stats)

# =========================
# 集計(平均・最大・最小)
# =========================
summary_df = pd.DataFrame({
    "Model": ["XGBoost", "LightGBM"],
    "Avg Depth": [xgb_stats_df["Depth"].mean(), lgb_stats_df["Depth"].mean()],
    "Max Depth": [xgb_stats_df["Depth"].max(), lgb_stats_df["Depth"].max()],
    "Min Depth": [xgb_stats_df["Depth"].min(), lgb_stats_df["Depth"].min()],
    "Avg Nodes": [xgb_stats_df["Nodes"].mean(), lgb_stats_df["Nodes"].mean()],
    "Max Nodes": [xgb_stats_df["Nodes"].max(), lgb_stats_df["Nodes"].max()],
    "Min Nodes": [xgb_stats_df["Nodes"].min(), lgb_stats_df["Nodes"].min()],
})

print("=== Tree Structure Summary ===")
print(summary_df)
出力例:全木を集計した統計値
=== Tree Structure Summary ===
      Model  Avg Depth  Max Depth  Min Depth  Avg Nodes  Max Nodes  Min Nodes
0   XGBoost   3.574286          7          1  10.562857         65          1
1  LightGBM   7.822857         11          3  23.611429         33          5
木 可視化

❓なぜ(Tree 0)なの❓

「0 番目の木を描いてますよ」 と言う意味です。

理由

  • XGBoost や LightGBM は、複数本の木の集合(勾配ブースティング木)で構成される
  • 学習後のモデルは「木が100本(num_boost_round=100)」のように多数ある
  • num_trees=0 → 最初の木(0 番目)を描画
  • num_trees=1 → 1 番目の木(2本目)を描画

補足

  • 表の Avg Depth / Max Depth / Min Depth は 全木を集計した統計値
  • 可視化は重いため、代表として Tree 0 だけを表示することが多い

コード:特徴量重要度~可視化
# ======================================
# LightGBM Feature Importance  
# ======================================
lgb_imp = pd.DataFrame({
    "特徴量": lgb_model.feature_name(),
    "重要度": lgb_model.feature_importance()
}).sort_values("重要度", ascending=False)
lgb_imp.rename(columns={"重要度":"LightGBM"}, inplace=True)

# ======================================
# XGBoost Feature Importance
# ======================================
xgb_imp = pd.Series(xgb_model.get_booster().get_score(importance_type="weight")).sort_values(ascending=False)
xgb_imp = xgb_imp.reset_index()
xgb_imp.columns = ["特徴量","XGBoost"]

# ======================================
# 両方を結合して比較
# ======================================
feature_compare = pd.merge(lgb_imp, xgb_imp, on="特徴量", how="outer").fillna(0)
feature_compare = feature_compare.sort_values("XGBoost", ascending=False)
shap_compare_norm = feature_compare.copy()

shap_compare_norm["LightGBM"] = shap_compare_norm["LightGBM"] / shap_compare_norm["LightGBM"].max()
shap_compare_norm["XGBoost"]  = shap_compare_norm["XGBoost"]  / shap_compare_norm["XGBoost"].max()

print("=== Normalized Feature Importance Comparison ===")
print(shap_compare_norm.sort_values("XGBoost", ascending=False).head(10))

# ======================================
#  正規化 (0~1)
# ======================================
feature_compare_norm = feature_compare.copy()
feature_compare_norm["LightGBM"] = feature_compare_norm["LightGBM"] / feature_compare_norm["LightGBM"].max()
feature_compare_norm["XGBoost"] = feature_compare_norm["XGBoost"] / feature_compare_norm["XGBoost"].max()

# ======================================
# 上位 N 特徴量を XGBoost 順に取得(上位10特徴量)
# ======================================
topN = 10
top_features = feature_compare_norm.sort_values("XGBoost", ascending=False)["特徴量"].head(topN).values
light_values = feature_compare_norm.set_index("特徴量").loc[top_features, "LightGBM"].values
xgb_values = feature_compare_norm.set_index("特徴量").loc[top_features, "XGBoost"].values

# ======================================
# 可視化
# ======================================
x = np.arange(topN)
width = 0.35

fig, ax = plt.subplots(figsize=(10,6))
ax.barh(x - width/2, light_values[::-1], width, label="LightGBM", color="salmon")
ax.barh(x + width/2, xgb_values[::-1], width, label="XGBoost", color="skyblue")

ax.set_yticks(x)
ax.set_yticklabels(top_features[::-1])
ax.set_xlabel("Normalized Feature Importance")
ax.set_title("Normalized Feature Importance Comparison (Top 10)")
ax.legend()

# 棒上に数値表示
for i in range(topN):
    ax.text(light_values[::-1][i]+0.02, x[i]-width/2, f"{light_values[::-1][i]:.2f}", va='center')
    ax.text(xgb_values[::-1][i]+0.02, x[i]+width/2, f"{xgb_values[::-1][i]:.2f}", va='center')

plt.tight_layout()
plt.show()
出力例
=== Normalized Feature Importance Comparison ===
                     特徴量   XGBoost  LightGBM
0                 income  1.000000  1.000000
1                    age  0.746955  0.626979
2  previous_manufacturer  0.434371  0.045598
3                  hobby  0.345061  0.024699
4         car_preference  0.254398  0.031666
5                 family  0.230041  0.183661
6                 region  0.223275  0.210893
7               children  0.133965  0.112730
8                 gender  0.131258  0.105130
9        employment_type  0.085250  0.095630


コード:モデル定義~Top-3 Accuracy 計算
# ======================================
# モデル定義
# ======================================

xgb_model = xgb.XGBClassifier(
    objective="multi:softprob",
    num_class=len(classes),
    n_estimators=100,
    use_label_encoder=False,
    eval_metric="mlogloss",
    enable_categorical=True,  
    random_state=42
)

# 全データで学習
xgb_model.fit(X_df, y_enc)

# ==============================
# XGBoost パラメータ設定
# ==============================
params = {
    "objective": "multi:softprob",  # 確率出力
    "num_class": len(classes),
    "eval_metric": "mlogloss",
    "verbosity": 0,
}

# ==============================
# 学習(100回で学習)
# ==============================
# xgb_model はすでに xgb.XGBClassifier で定義済みとします
xgb_model.set_params(n_estimators=100)
xgb_model.fit(X_df, y_enc)  # 全データで学習(可視化や予測用)

# ==============================
# 全データに対する予測
# ==============================
y_all_pred_prob = xgb_model.predict_proba(X_df)  # 予測確率
y_all_pred = np.argmax(y_all_pred_prob, axis=1)  # 予測クラスラベル

# ==============================
# 上位3候補の抽出
# ==============================
top3_classes = np.argsort(y_all_pred_prob, axis=1)[:, ::-1][:, :3]
top3_probs   = np.sort(y_all_pred_prob, axis=1)[:, ::-1][:, :3]

# ==============================
# 元データに結果を追加
# ==============================
df["predicted_manufacturer"] = y_all_pred

# 各クラス確率
for i in range(y_all_pred_prob.shape[1]):
    df[f"prob_class_{i}"] = y_all_pred_prob[:, i]

# 上位3クラス
for i in range(3):
    df[f"top{i+1}_class"] = top3_classes[:, i]
    df[f"top{i+1}_prob"] = top3_probs[:, i]

# 確認
print(df[[
    target_col,
    "predicted_manufacturer",
    "top1_class", "top1_prob",
    "top2_class", "top2_prob",
    "top3_class", "top3_prob"
]].head())

# ==============================
# Top-3 Accuracy 計算
# ==============================
top3_accuracy = np.mean([
    y_true in top3 for y_true, top3 in zip(y_enc, top3_classes)
])
print(f"Top-3 Accuracy: {top3_accuracy:.3f}")
出力例
  predicted_manufacturer  manufacturer  top1_class  top1_prob  top2_class  \
0             5                       5           5   0.989131           6   
1             6                       6           6   0.949702           5   
2             6                       6           6   0.989858           5   
3             2                       2           2   0.991235           5   
4             6                       6           6   0.969265           5   

   top2_prob  top3_class  top3_prob  
0   0.005029           0   0.002892  
1   0.046880           2   0.000909  
2   0.007324           0   0.000870  
3   0.004172           0   0.001390  
4   0.026152           3   0.002293  
Top-3 Accuracy: 1.000

コード:XGBoostモデル SHAP値計算~XGBoost 上位3寄与特徴量抽出

# ==============================
# XGBoostモデル SHAP値計算
# ==============================

# X_df のコピーを作ってカテゴリ列を cat.codes に変換
X_df_enc = X_df.copy()
for col in X_df_enc.select_dtypes(include='category'):
    X_df_enc[col] = X_df_enc[col].cat.codes

# SHAP explainer & shap値計算
xgb_explainer = shap.TreeExplainer(xgb_model)

# ✅ ここは X_df_enc を渡す!
shap_xgb_values = xgb_explainer.shap_values(X_df_enc)


if isinstance(shap_xgb_values, list):
    # マルチクラス分類の場合
    print("SHAP values shape per class:", [v.shape for v in shap_xgb_values])
else:
    # バイナリ or 回帰の場合
    print("SHAP values shape:", shap_xgb_values.shap
# =======================================
# 正解が top3 の何番目だったかを記録(XGBoost版)
# =======================================
def top_rank_xgb(row):
    true_class = row[target_col]
    top_classes = [row["top1_class"], row["top2_class"], row["top3_class"]]
    return top_classes.index(true_class) + 1 if true_class in top_classes else None

df["correct_rank"] = df.apply(top_rank_xgb, axis=1)

# 結果を確認
print(df["correct_rank"].value_counts())

# ======================================
# XGBoost 上位3寄与特徴量抽出
# ======================================

# 学習済みモデル: xgb_model
# 学習に使った特徴量: X_df (DataFrame)

# TreeExplainer の作成
explainer = shap.TreeExplainer(xgb_model)

# shap_values を計算
shap_values = explainer(X_df)  # ← これで shap_values が Explanation オブジェクトに入る


results_top3 = []

# shap_values は explainer(X_df) の結果(Explanationオブジェクト)
# multi-class の場合 shap_values.values は (n_samples, n_classes, n_features)
# binary の場合は (n_samples, n_features)

for i in range(len(df)):
    pred_class = df.loc[i, re_name_be]

    # multi-class なら、pred_class に対応する行を取得
    if len(class_names) > 2:  # multi-class
        shap_vals = shap_values.values[i, pred_class, :]
    else:  # binary
        shap_vals = shap_values.values[i, :]

    features = list(X_df.columns)

    # 絶対値で上位3特徴量を選択
    top_idx = np.argsort(np.abs(shap_vals))[::-1][:3]
    top_features = [features[j] for j in top_idx]
    top_values = [shap_vals[j] for j in top_idx]

    tmp = {
        ID: df.loc[i, ID],
        re_name_af: pred_class,
        "top1_feature": top_features[0],
        "top1_value": top_values[0],
        "top2_feature": top_features[1],
        "top2_value": top_values[1],
        "top3_feature": top_features[2],
        "top3_value": top_values[2]
    }
    results_top3.append(tmp)

top3_df = pd.DataFrame(results_top3)
top3_df.head()


コード:結合準備~predictions_with_top3_XGBoost.csv出力
# ==============================
# 結合準備
# ==============================
# すでに top3_df では "predicted_manufacturer" を使っているので、元データ df も揃える
#df = df.rename(columns={re_name_be: "predicted_manufacturer"})
# 列名を統一
df = df.rename(columns={"predicted_manufacturer": re_name_af})

# CSV出力(UTF-8 BOM付き)
df.to_csv("チェック用_XGBoost.csv", index=False, encoding="utf-8-sig")

# ==============================
# マージ(ID と予測クラス列で結合)
# ==============================
df_merged = pd.merge(df, top3_df, on=[ID, re_name_af], how="left")

# ==============================
# 列名リネーム(必要に応じて追加)
# ==============================
rename_dict = {
    "customer_id": "顧客ID",
    "family": "同居家族",
    "age": "年齢",
    "gender": "性別",
    "income": "世帯収入",
    "marital_status": "婚姻状況",
    "region": "居住地",
    "employment_type": "雇用形態",
    "hobby": "趣味",
    "car_preference": "好みのボディタイプ",
    "previous_car_owner": "保有歴",
    "top1_feature": "上位1特徴量",
    "top1_value": "上位1寄与度",
    "top2_feature": "上位2特徴量",
    "top2_value": "上位2寄与度",
    "top3_feature": "上位3特徴量",
    "top3_value": "上位3寄与度",
    "prob_class_0": "メーカー0の確率",
    "prob_class_1": "メーカー1の確率",
    "prob_class_2": "メーカー2の確率",
    "prob_class_3": "メーカー3の確率",
    "prob_class_4": "メーカー4の確率",
    "prob_class_5": "メーカー5の確率",
    "prob_class_6": "メーカー6の確率",
    "top1_class": "トップ1候補メーカー",
    "top1_prob": "トップ1確率",
    "top2_class": "トップ2候補メーカー",
    "top2_prob": "トップ2確率",
    "top3_class": "トップ3候補メーカー",
    "top3_prob": "トップ3確率",
}
df_merged = df_merged.rename(columns=rename_dict)

# ==============================
# CSV出力
# ==============================
df_merged.to_csv(save_path, index=False, encoding="utf-8-sig")
#print("✅ 完了:最終形態 CSV に SHAP上位特徴量も含めて保存しました!")

df_merged.head(3)


コード: XGBoost SHAP値 → DataFrame化~メーカー別_SHAP_特徴ランキングXGBoost.csv出力
# ==============================
# XGBoost SHAP値 → DataFrame化
# ==============================

# shap_values はすでに計算済み(xgb_explainer.shap_values(X_df_enc))
if isinstance(shap_xgb_values, list):  # multiclass
    # 予測クラスに対応する SHAP 値を抽出
    shap_array = np.array([
        shap_xgb_values[pred][i, :]
        for i, pred in enumerate(df["predicted_class"].values)
    ])
else:  # binary
    shap_array = shap_xgb_values

# DataFrame化(列名は X_df_enc に合わせる)
shap_xgb_df = pd.DataFrame(shap_array, columns=X_df_enc.columns)
shap_xgb_df["predicted_class"] = df["predicted_class"].values

# ==============================
# クラスごとの SHAP値平均(特徴量別)
# ==============================
summary_list = []
for cls in sorted(df["predicted_class"].unique()):
    shap_mean = (
        shap_xgb_df[shap_xgb_df["predicted_class"] == cls]
        .drop(columns="predicted_class")
        .mean()
        .abs()
    )
    shap_mean_sorted = shap_mean.sort_values(ascending=False)

    for feature, value in shap_mean_sorted.items():
        summary_list.append({
            "メーカー": cls,
            "特徴量": feature,
            "平均SHAP値": round(value, 5),
        })

shap_xgb_summary_df = pd.DataFrame(summary_list)

# ==============================
# 上位N個の特徴量を抽出
# ==============================
topN = 5
display_xgb_df = shap_xgb_summary_df.groupby("メーカー").head(topN)

# CSV保存
display_xgb_df.to_csv(save_name_path, index=False, encoding="utf-8-sig")

print("XGBoost SHAP集計 完了")

コード:クラス別 Top-1 精度~可視化(クラス別 Top-1 精度)
# ------------------------------
# クラス別 Top-1 精度
# ------------------------------
classes = sorted(df_xgb["manufacturer"].unique())

xgb_class_acc = [
    (df_xgb[df_xgb["manufacturer"] == cls]["predicted_class"] == cls).mean()
    for cls in classes
]
lgb_class_acc = [
    (df_lgb[df_lgb["manufacturer"] == cls]["predicted_class"] == cls).mean()
    for cls in classes
]

class_acc_df = pd.DataFrame({
    "Class": classes,
    "XGBoost Top-1 Accuracy": xgb_class_acc,
    "LightGBM Top-1 Accuracy": lgb_class_acc
})

print("=== Class-wise Top-1 Accuracy ===")
print(class_acc_df)

# ------------------------------
# 可視化(クラス別 Top-1 精度)
# ------------------------------
x = np.arange(len(classes))
width = 0.35

fig, ax = plt.subplots(figsize=(10,6))
rects1 = ax.bar(x - width/2, xgb_class_acc, width, label="XGBoost")
rects2 = ax.bar(x + width/2, lgb_class_acc, width, label="LightGBM")

ax.set_ylabel("Top-1 Accuracy")
ax.set_ylim(0,1)
ax.set_xticks(x)
ax.set_xticklabels(classes)
ax.set_title("Class-wise Top-1 Accuracy Comparison")
ax.legend()

for rects in [rects1, rects2]:
    for rect in rects:
        height = rect.get_height()
        ax.annotate(f'{height:.3f}',
                    xy=(rect.get_x() + rect.get_width()/2, height),
                    xytext=(0,3),
                    textcoords="offset points",
                    ha='center', va='bottom')

plt.tight_layout()
plt.show()
出力例
=== Class-wise Top-1 Accuracy ===
   Class  XGBoost Top-1 Accuracy  LightGBM Top-1 Accuracy
0      0                     1.0                 1.000000
1      1                     1.0                 1.000000
2      2                     1.0                 0.986486
3      3                     1.0                 0.833333
4      4                     1.0                 0.965517
5      5                     1.0                 0.882883
6      6                     1.0                 0.865385


コード:クラス別に予測件数(期待件数)を出した結果可視化~可視化
# ------------------------------
# クラス別に予測件数(期待件数)を出した結果
# 確率列の抽出(0~6のクラス)
# ------------------------------
df_xgb_copy = df_merged.copy()
df_lgb_copy = df_lgb.copy()

prob_cols = [f"メーカー{i}の確率" for i in range(7)]

# ------------------------------
# 期待購入件数計算
# ------------------------------
xgb_expected = df_xgb_copy[prob_cols].sum().reset_index()
xgb_expected.columns = ["メーカー", "XGBoost 期待件数"]

lgb_expected = df_lgb_copy[prob_cols].sum().reset_index()
lgb_expected.columns = ["メーカー", "LightGBM 期待件数"]

# 結合
expected_df = pd.merge(xgb_expected, lgb_expected, on="メーカー")
print(expected_df)

# ------------------------------
# 可視化
# ------------------------------
x = np.arange(len(expected_df))
width = 0.35

fig, ax = plt.subplots(figsize=(10,6))
rects1 = ax.bar(x - width/2, expected_df["XGBoost 期待件数"], width, label="XGBoost")
rects2 = ax.bar(x + width/2, expected_df["LightGBM 期待件数"], width, label="LightGBM")

ax.set_ylabel("期待購入件数(確率ベース)")
ax.set_xticks(x)
ax.set_xticklabels(expected_df["メーカー"])
ax.set_title("XGBoost vs LightGBM クラス別期待購入件数")
ax.legend()

# 棒の上に数値表示
for rects in [rects1, rects2]:
    for rect in rects:
        height = rect.get_height()
        ax.annotate(f'{height:.1f}',
                    xy=(rect.get_x() + rect.get_width()/2, height),
                    xytext=(0,3),
                    textcoords="offset points",
                    ha='center', va='bottom')

plt.tight_layout()
plt.show()

出力例
       メーカー  XGBoost 期待件数  LightGBM 期待件数
0  メーカー0の確率     13.002707      12.995387
1  メーカー1の確率     56.987980      57.044848
2  メーカー2の確率     74.000336      73.240391
3  メーカー3の確率     12.014658      10.133637
4  メーカー4の確率     29.020639      27.607897
5  メーカー5の確率    110.991791     117.337932
6  メーカー6の確率    103.981888     101.639907


コード: LightGBM SHAP値~可視化
# 寄与度比較
# ========================
# LightGBM SHAP値
# ========================
lgb_explainer = shap.TreeExplainer(lgb_model)
shap_lgb_values = lgb_explainer.shap_values(X_df)

# binary/multiclass の処理
if isinstance(shap_lgb_values, list):
    shap_lgb_array = np.array([shap_lgb_values[pred][i,:] for i,pred in enumerate(df_lgb["predicted_class"])])
else:
    shap_lgb_array = shap_lgb_values

shap_lgb_df = pd.DataFrame(np.abs(shap_lgb_array).mean(axis=0), index=X_df.columns, columns=["LightGBM"])

# ========================
# XGBoost SHAP値
# ========================
X_df_enc = X_df.copy()
for col in X_df_enc.select_dtypes(include="category"):
    X_df_enc[col] = X_df_enc[col].cat.codes

xgb_explainer = shap.TreeExplainer(xgb_model)
shap_xgb_values = xgb_explainer.shap_values(X_df_enc)

if isinstance(shap_xgb_values, list):
    shap_xgb_array = np.array([shap_xgb_values[pred][i,:] for i,pred in enumerate(df_xgb["predicted_class"])])
else:
    shap_xgb_array = shap_xgb_values

shap_xgb_df = pd.DataFrame(np.abs(shap_xgb_array).mean(axis=0), index=X_df_enc.columns, columns=["XGBoost"])

# ========================
# 比較
# ========================
shap_compare = shap_xgb_df.join(shap_lgb_df).sort_values("XGBoost", ascending=False)
print(shap_compare)

# ========================
# 可視化(棒グラフ)
# ========================
# 横棒グラフで可視化
#shap_compare.plot(kind="barh", figsize=(10, 8), width=0.7)

shap_compare.plot(kind="bar", figsize=(12,6))
plt.ylabel("平均SHAP値(寄与度)")
plt.title("XGBoost vs LightGBM 寄与度比較")
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
plt.show()
出力例
                        XGBoost  LightGBM
income                 1.260725  1.822286
car_preference         1.028896  1.118688
previous_manufacturer  0.728317  0.446252
age                    0.383150  0.309408
hobby                  0.321610  0.170742
region                 0.205471  0.242472
family                 0.147715  0.129417
gender                 0.105472  0.090702
children               0.055276  0.086637
employment_type        0.049248  0.062799
previous_car_owner     0.030030  0.114660
test_drive             0.028128  0.008967
marital_status         0.000000  0.028145


📌精度と特徴量寄与の比較

❓指標の意味❓

Top-k 精度

  • Top-1 精度:1位の予測が正解と一致した割合(Accuracy に近い)
  • Top-3 精度:予測上位3位のいずれかが正解と一致した割合
    • 「正解が候補上位に入っているか」を確認できる

Accuracy / F1 Score

  • Accuracy = 正解と一致した割合
  • F1 Score = クラス間の不均衡を考慮した平均精度
  • Top-1 精度と概念的に近い

✅ 全体精度(今回のデータ)

  • Top-1 精度は LightGBM 0.73、XGBoost 0.72(例)
  • Top-3 精度もほぼ同等
  • 精度だけを見ると、両モデルに大きな差はない

✅ クラス別精度

  • XGBoost:全クラスで安定して高い精度
  • LightGBM:一部のクラスで高い精度、低い精度のばらつきあり

✅ 特徴量寄与(SHAP値)

  • XGBoost:主要特徴量に寄与が集中、少数の特徴量で判断
  • LightGBM:カテゴリを含め、上位特徴量がいくつかに分散

✅ 結論

  • 精度だけなら両モデルほぼ同じ
  • SHAP値を見ると、モデルごとに特徴量の使い方に違いがある
  • データや目的に応じて、精度・解釈性・速度のバランスで選ぶのが現実的
  • 実務では両方試して比較するのが安心

📌まとめ

もう少し情報があれば、販売予測も可能です。
サンプルデータの準備できたら、予測パートも投稿する予定です。

Discussion