📚 背景知識(読んでから問題へ)
feature_importances_)の弱点: これは「学習データの中で分岐に使われた回数・不純度減少量」の合計に過ぎません。①学習データにしか存在しないノイズによる偶然の分岐も加点される(in-sampleの数値)、②連続値や高カーディナリティ(値の種類が多い)な特徴量ほど分岐の候補が多く有利になりやすい、という構造的なバイアスがあります。今日学ぶ3手法はこのバイアスを避けるためのものです。| 用語 | 直感的な意味 |
|---|---|
| Permutation Importance | ある特徴量だけをランダムにシャッフルし、testデータでモデル性能がどれだけ落ちるかを測る手法 |
| Boruta | 全特徴量の「シャッフルしたコピー(シャドウ特徴量)」を用意し、本物の重要度がシャドウ集団の最大値を上回るかを繰り返し検証する手法 |
| SHAP(SHapley Additive exPlanations) | ゲーム理論のShapley値を応用し、各特徴量が「1件の予測」にどれだけ貢献したかを公平に配分する手法 |
| mean(|SHAP値|) | 個々の予測へのSHAP貢献度の絶対値を全サンプルで平均した、SHAPの「グローバル重要度」版 |
🧩 「本物の特徴量」を見極める3つの検証手法
🔀 Permutation Importance
シャッフルして性能低下を見る
testデータで特徴量を1列ずつシャッフルし、AUC等の性能がどれだけ落ちるかを測る。out-of-sampleなので組み込み重要度のバイアスを受けにくい。モデル非依存で使える。
🌑 Boruta(シャドウ特徴量)
ランダムなダミー集団と比較する
全特徴量のシャッフル版(シャドウ)を作り、本物の重要度がシャドウ集団の最大値を何度上回るかをカウント。「偶然では説明できない」ことを統計的に検証する発想。
🎲 SHAP
Shapley値で貢献度を公平配分
1件の予測に対し、各特徴量がどれだけ寄与したかをゲーム理論に基づき公平に配分する。グローバル(全体傾向)にもローカル(個別予測)にも使える唯一の手法。
🎯 問題
9個の特徴量(本物の情報を持つ信号5個・純粋なノイズ3個・値の種類が非常に多い疑似ID列1個)の中から「本当に効いている特徴量」を4つの視点で見極める。
import numpy as np import pandas as pd from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split RANDOM_STATE = 42 rng = np.random.RandomState(RANDOM_STATE) # 5個の「本物」信号特徴量 X_base, y = make_classification( n_samples=3000, n_features=5, n_informative=5, n_redundant=0, n_clusters_per_class=2, class_sep=1.2, random_state=RANDOM_STATE ) feat_names = [f"Signal{i+1}" for i in range(5)] df = pd.DataFrame(X_base, columns=feat_names) # 3個の純粋なノイズ特徴量(yと無関係な標準正規乱数) for i in range(3): df[f"Noise{i+1}"] = rng.normal(0, 1, size=len(df)) # 1個の高カーディナリティな疑似ID列 df["HighCardID"] = rng.randint(0, 100000, size=len(df)) FEATURES = list(df.columns) X = df[FEATURES].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=RANDOM_STATE )
| カラム | 意味 | 真の関係 |
|---|---|---|
| Signal1〜Signal5 | make_classificationが生成した本物の情報特徴量 | yと強く関係する |
| Noise1〜Noise3 | 標準正規分布からの独立な乱数 | yと完全に無関係 |
| HighCardID | 0〜99999のランダムな整数(transaction_id等の誤混入を模擬) | yと完全に無関係・値の種類が非常に多い |
タスク
RandomForestClassifier(n_estimators=300, max_depth=6)を学習し、test AUCを算出するfeature_importances_を取得し、9特徴量をランキングするpermutation_importanceをtestデータにscoring="roc_auc", n_repeats=30で計算するshap.TreeExplainerでtestデータのSHAP値を計算し、mean(|SHAP値|)でランキングする📐 組み込み重要度 vs Permutation Importance(各系列の最大値を100%として正規化)
実際に計算した値をもとに作図。オレンジ=組み込み重要度、青=Permutation Importance。ノイズ系4特徴量(右側4組)で、オレンジは薄く残るのに青はほぼ消えることに注目。
📊 実行結果の比較(4手法)
実際にコードを実行して得た値(乱数シード固定・環境により多少前後する)。
Test AUC
組み込み重要度 vs Permutation Importance(下位4特徴量に注目)
組み込み重要度では「高カーディナリティのHighCardID」より「ただの連続値ノイズNoise3」の方が高い値になった。特徴量の種類にかかわらず、in-sampleの数値は偶然の相関を拾いうる。
Boruta風シャドウ特徴量テスト(20試行中の勝利回数)
SHAP: mean(|SHAP値|)
SHAPのランキングはPermutation Importanceとほぼ同じ並びになった。Signal群(0.05〜0.11)とノイズ群(0.0016〜0.0041)の間に明確な断絶があり、判断がしやすい。
💡 ヒント
feature_importances_は「学習データの中で、その特徴量による分岐がどれだけ不純度を減らしたか」の合計。学習データだけを見て計算されるため、偶然の相関を「重要」と誤認しうる。Permutation Importanceは未知のtestデータでシャッフル前後の性能差を見るため、より正直に汎化性能への寄与を反映する。2つの手法が大きく違う評価をする場合、信じるべきは基本的に後者(out-of-sample)。
from sklearn.inspection import permutation_importance。result = permutation_importance(model, X_test, y_test, n_repeats=30, scoring="roc_auc", random_state=42)でresult.importances_mean・result.importances_stdが得られる- シャドウ特徴量は「各列を独立にシャッフルしたコピー」。
np.random.permutation(X_train[:, j])を各列ごとに適用し、np.hstack([X_train, X_shadow])で元の特徴量と横に連結してから学習する - Boruta風の判定は「各試行で、本物の特徴量
iの重要度 > シャドウ特徴量全体の最大重要度」をTrue/Falseで記録し、複数試行の中でTrueだった回数を数える - SHAPは
explainer = shap.TreeExplainer(model),shap_values = explainer.shap_values(X_test)。sklearn/shapのバージョンによって返り値の形(クラスごとのリスト or 3次元配列)が異なるので分岐が必要な場合がある
from sklearn.ensemble import RandomForestClassifier from sklearn.inspection import permutation_importance from sklearn.metrics import roc_auc_score import shap # 1. ベースライン rf = RandomForestClassifier(n_estimators=300, max_depth=6, random_state=42, n_jobs=-1) rf.fit(X_train, y_train) auc = roc_auc_score(y_test, rf.predict_proba(X_test)[:, 1]) # 2. 組み込み重要度 imp_builtin = pd.Series(rf.feature_importances_, index=FEATURES).sort_values(ascending=False) # 3. Permutation Importance(testデータ) perm = permutation_importance(rf, X_test, y_test, n_repeats=30, random_state=42, scoring="roc_auc") imp_perm = pd.Series(perm.importances_mean, index=FEATURES).sort_values(ascending=False) # 4. Boruta風シャドウ特徴量テスト N_TRIALS = 20 hit_counts = pd.Series(0, index=FEATURES) for trial in range(N_TRIALS): trial_rng = np.random.RandomState(1000 + trial) X_shadow = np.column_stack([trial_rng.permutation(X_train[:, j]) for j in range(X_train.shape[1])]) X_combined = np.hstack([X_train, X_shadow]) combined_names = FEATURES + [f"shadow_{f}" for f in FEATURES] rf_shadow = RandomForestClassifier(n_estimators=150, max_depth=6, random_state=trial_rng, n_jobs=-1) rf_shadow.fit(X_combined, y_train) imp_combined = pd.Series(rf_shadow.feature_importances_, index=combined_names) max_shadow = imp_combined[[f"shadow_{f}" for f in FEATURES]].max() for f in FEATURES: if imp_combined[f] > max_shadow: hit_counts[f] += 1 # 5. SHAP explainer = shap.TreeExplainer(rf) shap_values = explainer.shap_values(X_test) sv = shap_values[1] if isinstance(shap_values, list) else shap_values[:, :, 1] mean_abs_shap = pd.Series(np.abs(sv).mean(axis=0), index=FEATURES).sort_values(ascending=False)
✅ 模範解答
import numpy as np import pandas as pd from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.inspection import permutation_importance from sklearn.metrics import roc_auc_score import shap RANDOM_STATE = 42 rng = np.random.RandomState(RANDOM_STATE) # ====== データ準備 ====== X_base, y = make_classification( n_samples=3000, n_features=5, n_informative=5, n_redundant=0, n_clusters_per_class=2, class_sep=1.2, random_state=RANDOM_STATE ) feat_names = [f"Signal{i+1}" for i in range(5)] df = pd.DataFrame(X_base, columns=feat_names) for i in range(3): df[f"Noise{i+1}"] = rng.normal(0, 1, size=len(df)) df["HighCardID"] = rng.randint(0, 100000, size=len(df)) FEATURES = list(df.columns) X = df[FEATURES].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=RANDOM_STATE ) # ====== 1. ベースラインモデル ====== rf = RandomForestClassifier(n_estimators=300, max_depth=6, random_state=RANDOM_STATE, n_jobs=-1) rf.fit(X_train, y_train) auc = roc_auc_score(y_test, rf.predict_proba(X_test)[:, 1]) print(f"Test AUC: {auc:.4f}") # ====== 2. 組み込み重要度 ====== imp_builtin = pd.Series(rf.feature_importances_, index=FEATURES).sort_values(ascending=False) # ====== 3. Permutation Importance(testデータ、n_repeats=30) ====== perm = permutation_importance(rf, X_test, y_test, n_repeats=30, random_state=RANDOM_STATE, scoring="roc_auc", n_jobs=-1) imp_perm = pd.Series(perm.importances_mean, index=FEATURES).sort_values(ascending=False) # ====== 4. Boruta風シャドウ特徴量テスト(20試行) ====== N_TRIALS = 20 hit_counts = pd.Series(0, index=FEATURES) for trial in range(N_TRIALS): trial_rng = np.random.RandomState(1000 + trial) X_shadow = np.column_stack([trial_rng.permutation(X_train[:, j]) for j in range(X_train.shape[1])]) X_combined = np.hstack([X_train, X_shadow]) combined_names = FEATURES + [f"shadow_{f}" for f in FEATURES] rf_shadow = RandomForestClassifier(n_estimators=150, max_depth=6, random_state=trial_rng, n_jobs=-1) rf_shadow.fit(X_combined, y_train) imp_combined = pd.Series(rf_shadow.feature_importances_, index=combined_names) max_shadow = imp_combined[[f"shadow_{f}" for f in FEATURES]].max() for f in FEATURES: if imp_combined[f] > max_shadow: hit_counts[f] += 1 # ====== 5. SHAP ====== explainer = shap.TreeExplainer(rf) shap_values = explainer.shap_values(X_test) sv = shap_values[1] if isinstance(shap_values, list) else shap_values[:, :, 1] mean_abs_shap = pd.Series(np.abs(sv).mean(axis=0), index=FEATURES).sort_values(ascending=False) # ====== 6. 4手法の比較表 ====== comparison = pd.DataFrame({ "builtin_rank": imp_builtin.rank(ascending=False).astype(int), "builtin_value": imp_builtin, "perm_rank": imp_perm.rank(ascending=False).astype(int), "perm_value": imp_perm, "boruta_hits/20": hit_counts, "shap_rank": mean_abs_shap.rank(ascending=False).astype(int), "shap_value": mean_abs_shap, }).loc[FEATURES].sort_values("perm_rank") print(comparison.round(4).to_string())
実際に実行して得た出力(RANDOM_STATE=42固定・環境により多少前後する):
Test AUC: 0.9487 builtin_rank builtin_value perm_rank perm_value boruta_hits/20 shap_rank shap_value Signal2 1 0.2159 1 0.1268 20 1 0.1140 Signal3 3 0.1929 2 0.1123 20 2 0.0922 Signal1 5 0.1530 3 0.0938 20 5 0.0520 Signal4 4 0.1826 4 0.0680 20 4 0.0713 Signal5 2 0.2054 5 0.0649 20 3 0.0853 HighCardID 7 0.0124 6 0.0003 6 7 0.0036 Noise1 9 0.0120 7 0.0002 0 9 0.0016 Noise2 8 0.0123 8 0.0000 1 8 0.0023 Noise3 6 0.0136 9 -0.0001 10 6 0.0041
Noise3(0.0136)がHighCardID(0.0124)より高く、「高カーディナリティだけが危ない」という単純な思い込みは誤りだと分かります。Permutation ImportanceとSHAPはNoise1・Noise2・HighCardIDをほぼゼロと明確に判定し、Boruta風テストでも0〜6/20回と低い水準でした。唯一Noise3だけがBorutaで10/20回という境界値になり、これは試行回数を増やして再検証すべき対象です。🪜 Step-by-Step 解説
1ベースラインモデルを学習する
rf = RandomForestClassifier(n_estimators=300, max_depth=6, random_state=42, n_jobs=-1) rf.fit(X_train, y_train) # Test AUC: 0.9487
max_depth=6で意図的に木の深さを制限している。制限しない場合、木は容易に訓練データのノイズにまで適合してしまい(過学習)、後続の4手法の比較がより極端になる。Test AUC=0.9487と十分に高い性能が出ており、「そこそこ良いモデル」の内部で何が起きているかを分析する土台になっている。2組み込み重要度を確認する — 一見もっともらしいが、罠がある
Signal2 0.2159, Signal5 0.2054, ... Noise3 0.0136, HighCardID 0.0124, Noise2 0.0123, Noise1 0.0120
Signal特徴量が上位を占めるのは正しい傾向に見えるが、注目すべきは下位。Noise3(純粋なノイズ)がHighCardIDより高い重要度(0.0136 > 0.0124)を得ている。訓練データの中でたまたま生じた偶然の相関を拾ってしまえば、どんな種類の特徴量でも組み込み重要度は非ゼロの値を返してしまう。3Permutation Importanceで「本当に効いているか」を検証する
HighCardID : mean=0.0003 Noise1 : mean=0.0002 Noise2 : mean=0.0000 Noise3 : mean=-0.0001
Noise3にいたっては負の値)。組み込み重要度ではNoise3が0.0136という無視できない値だったのに対し、out-of-sampleで検証すると実際には何の予測力もないことが明確になる。これがPermutation Importanceの核心的な価値。4Boruta風シャドウ特徴量テストで統計的に判定する
Signal1〜5: 20/20回すべてでシャドウの最大値を上回った Noise3: 10/20回(五分五分) HighCardID: 6/20回 Noise2: 1/20回 Noise1: 0/20回
Signal特徴量は20試行すべてでシャドウ特徴量の最大値を上回り、疑いの余地なく「本物」と判定できる。一方Noise3は10/20回と五分五分——偶然そのもの(ノイズなので、シャドウ集団の中のたまたま一番強いノイズより強い日もあれば弱い日もある)。本家のBorutaパッケージはこの「五分五分」を厳密な二項検定で判定し、既定では最大100試行を回す。今回の簡易実装(20試行)で境界事例が生まれるのは自然で、試行回数を増やすほど判定は安定する。5SHAPでモデルの判断根拠を特徴量に分配する
Signal2 0.1140 … Noise3 0.0041, HighCardID 0.0036, Noise2 0.0023, Noise1 0.0016
mean(|SHAP値|)は「予測値にどれだけ影響したか」の平均的な大きさを表すのに対し、Permutation Importanceは「性能指標(AUC)がどれだけ落ちるか」を表す。測っているものが違うのに、今回はランキングがほぼ一致したことで、この2手法の結論に対する確信が強まる。64手法をまとめて「残すべき特徴量」を決める
Noise1・Noise2・HighCardIDは自信を持って除外できる。Noise3だけは唯一Boruta風テストで境界値になったため、試行回数を増やして再検証するか保守的に除外するかの判断が必要——1つの手法だけで決め切らず、複数手法が一致する部分を信頼するのが今日一番の学び。🧮 数学・統計の補足(文系向け)
数式を見たら
PermImp(f) = Score(y, ŷ) − Score(y, ŷ_f shuffled)
「特徴量fをシャッフルする前のスコア」から「シャッフルした後のスコア」を引いたもの。効いている特徴量ほどシャッフルでスコアが大きく下がる(差が大きい)ため、この差=重要度になる。
φ_i = Σ_S [ |S|!(n−|S|−1)! / n! ] × [v(S∪{i}) − v(S)]
これがShapley値(SHAPの理論的な土台)。文系向けに言い換えると「特徴量iを、他の特徴量のあらゆる組み合わせの順番に追加したときの貢献度の平均」。チームの成果(賞金)をメンバー全員に公平に配分する際、「この人が加入する前と後でチームの成果がどれだけ変わったか」を、あらゆる加入順序で計算して平均する——という考え方と同じ。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 画像認識(CV) / ☑ 時系列(Time Series)
- 特徴量エンジニアリング後の絞り込み: 集計特徴量・Rolling/Lag特徴量(Day 099・100)を大量に生成した後、Permutation ImportanceかSHAPの
mean(|SHAP値|)が低い特徴量を機械的に足切りし、上位N個だけで再学習することでCVスコアと学習速度の両方が改善することが多い - Boruta(
boruta_py): 過去の上位解法ノートブックで「Borutaで数百の候補特徴量から本物の特徴量だけを抽出した」という報告は頻出。計算コストが高い(多数のモデルを繰り返し学習する)ため、GBDT系の高速版と組み合わせて使うのが実務的 - SHAPは「特徴量選択」と「モデル解釈」の二役: Kaggleの最終レポート(Solution write-up)ではSHAP summary plotで「どの特徴量がなぜ効いたか」を説明するのが定番。Public/Privateのシェイクアップ対策として、特定の1〜2個の特徴量にSHAP依存度が偏りすぎていないか確認するのにも使われる
- 相関の強い特徴量への注意: Permutation Importanceは、ある特徴量をシャッフルしても相関する別の特徴量が同じ情報を持っていれば「性能があまり落ちない=重要度が低く見える」という弱点がある。強相関のグループはまとめてシャッフルする「grouped permutation importance」で対処するのが上位陣のテクニック
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
feature_importances_が高ければ、その特徴量は本物の予測力を持つと信じてよい | sklearnが標準搭載しており最も手軽に見られる指標のため | 今日の実行結果では純粋なノイズであるNoise3(0.0136)が高カーディナリティのHighCardID(0.0124)より高い値になった。組み込み重要度は訓練データ内の偶然の相関にも反応するため、単独で判断材料にしてはいけない |
| 高カーディナリティな特徴量だけが組み込み重要度のバイアスを受けると思う | 教科書的に「IDのような列が危ない」という説明をよく見るため | 今回のように、特別高カーディナリティでなくてもただの連続値ノイズが偶然高い重要度を得ることがある。特徴量の種類にかかわらず、out-of-sampleで検証する習慣が必要 |
| Permutation Importanceが0や負の値になった特徴量は完全に無価値だと断定してよい | 「数値が0=無関係」という直感 | 今回のように真にノイズなら妥当な結論だが、相関の強い特徴量が他にある場合、片方をシャッフルしても情報が別の特徴量から補われ、見かけ上重要度が低く出ることがある。相関構造も合わせて確認する必要がある |
| Boruta風テストは1回や少数回の試行で確定的な結論を出せる | 「1回でも基準を満たせば十分」という思い込み | 今日のNoise3(20回中10回)のように、試行回数が少ないと純粋なノイズでも五分五分の結果が出ることがある。本家Borutaが多数回の試行と二項検定を使うのはこのため |
| SHAPとPermutation Importanceは常に同じランキングになるはずだ | どちらも「特徴量の重要度」を測る手法という理解にとどまっているため | SHAPは「予測値への貢献」、Permutation Importanceは「性能指標の低下量」を測っており、測定対象が異なる。今日はたまたまほぼ一致したが、特徴量間の相関が強い場合など、ランキングが食い違うことは珍しくない |
🚀 次のステップ
- 発展:
Signal1〜5同士に人工的な相関を持たせたデータ(例:Signal1_dup = Signal1 + 少量のノイズ)を追加し、Permutation Importanceで元のSignal1とSignal1_dupの重要度がどう分散するかを確認してみる。相関特徴量が重要度を「薄める」現象を体験できる - 次回予告: Day 108「SHAP値の解釈(グローバル/ローカル解釈・Kaggleでの活用)」 — 今日はモデル全体の傾向(グローバル)だけを見たが、次回は1件の予測ごとに「なぜこの予測になったか」を説明するローカル解釈(force plot・waterfall plot)とKaggleでの具体的な活用法に進む
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: