Day 060 — ランダムフォレスト — バギング・特徴量重要度・OOBスコア

2026-06-10 緑 / Phase 2 コーディング RandomForest・Bagging・OOB・feature_importances

📚 背景知識(読んでから問題へ)

🎯
Day 060 — Day 059 で学んだ決定木の弱点(過学習)を克服するランダムフォレストを学びます。「多数決」と「ランダム性の導入」がキーワードです。

ランダムフォレスト = 決定木の集合知

1人の専門家の意見より、100人の専門家の多数決のほうが正確です(集合知)。ランダムフォレストは決定木を100本作って多数決します。ただし各木が同じ意見を持つと意味がないので、ランダム性を2つの方法で導入します。

ランダム性の種類内容効果
ブートストラップサンプリング各木が異なるデータ(重複あり)で学習木間の相関を低下
特徴量のランダム選択各分岐で sqrt(n_features) の特徴量のみ使用木間の多様性を確保

OOBスコア — 無料のバリデーション

ブートストラップでは全データの約 36.8% が選ばれません(Out-of-Bag)。この未使用データが自動的にバリデーションセットになります。

つまり oob_score=True を設定するだけで、交差検証なしにモデル性能を推定できます。

🌳 バギングの流れ(n_estimators=3 の例)

実際は 100〜500 本の木を作って多数決。各木は異なるデータセットで学習。

元データ
N=891件
全データ
木①
Bootstrap
サンプル①
(N件・重複あり)
木②
Bootstrap
サンプル②
(N件・重複あり)
木③
Bootstrap
サンプル③
(N件・重複あり)
多数決
①②③の
予測を集計
→ 最終予測
全データ N=891 木① ~63%のデータ 木② ~63%のデータ 木③ ~63%のデータ 多数決 最終予測 各木の未使用 ≈ 37% = OOBサンプル 予測

📊 OOBスコアの仕組み

各サンプルについて「そのサンプルを学習に使わなかった木のみ」で予測 → 全体の予測精度 = OOBスコア

サンプル 木① 木② 木③ 木④ OOB予測 Sample A 学習済 OOB→評価 OOB→評価 学習済 木②③の多数決 Sample B OOB→評価 学習済 学習済 OOB→評価 木①④の多数決 = そのサンプルが学習データに含まれる = OOBサンプル(学習に未使用 → 評価に使う)

OOBスコア vs 5-Fold CV精度の比較

5-Fold CV 精度
≈0.83
5回学習
OOBスコア(自動)
≈0.82
1回学習

OOBは CVと近い値を1回の学習で推定できるが、厳密には同等ではない。

📉 木の数と分散低減(n_estimators の効果)

n_estimators が増えるほど CV精度は安定するが、ある数以上は頭打ちになる。

n_estimators(木の本数) 0.75 0.80 0.85 0.90 10 50 100 200 500 CV精度 収束ポイント付近 高分散

観察: 10本は不安定、100本で概ね収束。500本は計算コストが5倍になるが精度向上は微小。

特徴量重要度(RF vs 決定木)

RFは100本の平均なので安定。決定木は1本なので偏りが出やすい。

RandomForest (n=100)

Sex_enc(性別)
0.58
最重要
Fare(運賃)
0.20
2位
Age(年齢)
0.12
3位
Pclass(客室等級)
0.08
4位
SibSp(同乗家族数)
0.02
5位

⚙️ 主要パラメータ

n_estimators
木の本数。デフォルト=100。多いほど安定するが計算コスト増。Kaggleでは 500〜1000 が多い。
max_features
各分岐で使う特徴量数。分類='sqrt'(デフォルト)、回帰='1.0'。小さいほど木間の多様性が増す。
oob_score
True にするとOOBスコアを計算。CVの代替として高速評価可能。デフォルト=False。
max_depth
None が標準(制限なし)。バギングで過学習が抑制されるため決定木と違い深くてOK。
min_samples_leaf
葉ノードの最小サンプル数。デフォルト=1。大きくするとメモリ節約・速度向上。
n_jobs
必ず -1 を設定(全CPUコアを並列使用)。大幅な速度向上。デフォルト=1(シングルコア)。

🗂️ データスキーマ(Titanic 疑似データ)

列名値の範囲説明RFでの重要度(目安)
Pclassint1, 2, 3旅客クラス
Sex_encint0 / 1性別のラベルエンコード最高
Agefloat1〜80年齢
SibSpint0〜8同乗している兄弟/配偶者数
Farefloat0〜∞運賃(Pclassと相関あり)中〜高
Survivedint0 / 1生存(目的変数)

クラス分布

死亡(0) ≈ 62% 生存(1) ≈ 38%

📝 問題

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, classification_report
import warnings
warnings.filterwarnings('ignore')

# Titanic データの簡易作成
np.random.seed(42)
n = 891

data = pd.DataFrame({
    'Pclass': np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
    'Sex':    np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
    'Age':    np.random.normal(29.7, 14.5, n).clip(1, 80),
    'SibSp':  np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
    'Fare':   np.abs(np.random.normal(32, 50, n)),
})
data['Survived'] = (
    (data['Sex'] == 'female') * 0.5 +
    (data['Pclass'] == 1) * 0.3 +
    (data['Age'] < 16) * 0.2 +
    np.random.normal(0, 0.15, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)
data['Sex_enc'] = LabelEncoder().fit_transform(data['Sex'])
features = ['Pclass', 'Sex_enc', 'Age', 'SibSp', 'Fare']
X = data[features].values
y = data['Survived'].values

問1 — 決定木 vs ランダムフォレスト

  1. DecisionTreeClassifier(random_state=42)RandomForestClassifier(n_estimators=100, random_state=42)StratifiedKFold(5) で比較せよ
  2. 訓練精度とCV精度の差(過学習度)をそれぞれ計算せよ
  3. ランダムフォレストが安定している理由を述べよ

問2 — n_estimators のチューニング

  1. n_estimators[10, 50, 100, 200, 500] で変化させながら CV スコアを記録せよ
  2. スコアの変化を観察し、「どこで頭打ちになるか」を確認せよ
  3. 計算コストと精度のトレードオフを述べよ

問3 — OOBスコアの活用

  1. RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42) で学習せよ
  2. clf.oob_score_ を取得し、StratifiedKFold(5) のCV精度と比較せよ
  3. OOBスコアがCVの代替として使える理由を説明せよ

問4 — 特徴量重要度の比較

  1. ランダムフォレストの feature_importances_ を取得し、重要度順にソートして出力せよ
  2. Day 059 の決定木(max_depth=5)の特徴量重要度と比較せよ
  3. ランダムフォレストの重要度のほうが安定している理由を述べよ

問5 — max_features の効果

  1. max_features['sqrt', 'log2', None, 0.3, 0.5] で変化させながらCV精度を比較せよ
  2. max_features=None(全特徴量使用)と 'sqrt' の違いを説明せよ
  3. Kaggleでは通常どの値を使うか述べよ

💡 ヒント

ヒント1(方向性)
  • OOBスコア取得には oob_score=True を指定してから fit() した後に clf.oob_score_ でアクセス
  • ランダムフォレストは fit() で全ての木を学習するため n_estimators が大きいほど遅い
  • feature_importances_ は全ての木の重要度の平均なので決定木より安定している
ヒント2(アプローチ)
# 決定木 vs ランダムフォレストの比較
cv = StratifiedKFold(5, shuffle=True, random_state=42)

for name, clf in [
    ("決定木", DecisionTreeClassifier(random_state=42)),
    ("RF", RandomForestClassifier(n_estimators=100, random_state=42))
]:
    clf.fit(X, y)
    tr = clf.score(X, y)
    cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
    print(f"[{name}] 訓練: {tr:.4f}, CV: {cv_s:.4f}, 差: {tr-cv_s:.4f}")
ヒント3(コード骨格)
# n_estimators チューニング
for n_est in [10, 50, 100, 200, 500]:
    clf = RandomForestClassifier(n_estimators=n_est, random_state=42)
    cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
    print(f"n_estimators={n_est:4d}: CV={cv_s:.4f}")

# OOBスコア
rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X, y)
print(f"OOB Score: {rf_oob.oob_score_:.4f}")

# 特徴量重要度(ランダムフォレスト vs 決定木)
for f, imp in sorted(zip(features, rf_oob.feature_importances_), key=lambda x: -x[1]):
    print(f"{f}: {imp:.4f}")

模範解答

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
import warnings
warnings.filterwarnings('ignore')

# データ準備
np.random.seed(42)
n = 891
data = pd.DataFrame({
    'Pclass': np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
    'Sex':    np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
    'Age':    np.random.normal(29.7, 14.5, n).clip(1, 80),
    'SibSp':  np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
    'Fare':   np.abs(np.random.normal(32, 50, n)),
})
data['Survived'] = (
    (data['Sex'] == 'female') * 0.5 + (data['Pclass'] == 1) * 0.3 +
    (data['Age'] < 16) * 0.2 + np.random.normal(0, 0.15, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)
data['Sex_enc'] = LabelEncoder().fit_transform(data['Sex'])
features = ['Pclass', 'Sex_enc', 'Age', 'SibSp', 'Fare']
X = data[features].values
y = data['Survived'].values

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# ── 問1: 決定木 vs ランダムフォレスト ──
print("=" * 60)
print("問1: 決定木 vs ランダムフォレスト")
print("=" * 60)
for name, clf in [
    ("DecisionTree(制限なし)", DecisionTreeClassifier(random_state=42)),
    ("RandomForest(n=100)",   RandomForestClassifier(n_estimators=100, random_state=42))
]:
    clf.fit(X, y)
    tr = clf.score(X, y)
    cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
    print(f"[{name}]")
    print(f"  訓練精度: {tr:.4f}, CV精度: {cv_s:.4f}, 過学習度: {tr-cv_s:.4f}")
print("\n→ RFは個々の木が過学習しても、多数決で誤差がキャンセルされるため安定する")

# ── 問2: n_estimators のチューニング ──
print("\n" + "=" * 60)
print("問2: n_estimators のチューニング")
print("=" * 60)
for n_est in [10, 50, 100, 200, 500]:
    clf = RandomForestClassifier(n_estimators=n_est, random_state=42)
    cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
    print(f"  n_estimators={n_est:4d}: CV精度 = {cv_s:.4f}")
print("\n→ 100〜200 あたりで頭打ちになる。500 は計算コストが増えるが精度向上は微小")

# ── 問3: OOBスコア ──
print("\n" + "=" * 60)
print("問3: OOBスコア vs CV精度")
print("=" * 60)
rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X, y)
oob = rf_oob.oob_score_
cv_s = cross_val_score(rf_oob, X, y, cv=cv, scoring='accuracy').mean()
print(f"  OOBスコア: {oob:.4f}")
print(f"  CV精度:    {cv_s:.4f}")
print(f"  差:        {abs(oob - cv_s):.4f}")
print("\n→ OOBはブートストラップで選ばれなかったデータで自動評価するため、")
print("  CV不要で似た推定が得られる(計算コストが低い)")

# ── 問4: 特徴量重要度の比較 ──
print("\n" + "=" * 60)
print("問4: 特徴量重要度(RF vs 決定木)")
print("=" * 60)

print("[RandomForest]")
rf_imp = sorted(zip(features, rf_oob.feature_importances_), key=lambda x: -x[1])
for f, imp in rf_imp:
    bar = "█" * int(imp * 40)
    print(f"  {f:10s}: {imp:.4f} {bar}")

print("\n[DecisionTree max_depth=5]")
dt = DecisionTreeClassifier(max_depth=5, random_state=42)
dt.fit(X, y)
dt_imp = sorted(zip(features, dt.feature_importances_), key=lambda x: -x[1])
for f, imp in dt_imp:
    bar = "█" * int(imp * 40)
    print(f"  {f:10s}: {imp:.4f} {bar}")
print("\n→ RFは100本の木の平均なので重要度が安定(分散が小さい)")

# ── 問5: max_features の効果 ──
print("\n" + "=" * 60)
print("問5: max_features の効果")
print("=" * 60)
for mf in ['sqrt', 'log2', None, 0.3, 0.5]:
    clf = RandomForestClassifier(n_estimators=100, max_features=mf, random_state=42)
    cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
    print(f"  max_features={str(mf):6s}: CV精度 = {cv_s:.4f}")
print("\n→ 'sqrt'がデフォルト。Noneは全特徴量を使い木間の相関が高くなる(多様性低下)")
print("  Kaggleでは'sqrt'か0.3〜0.5が標準的")

🪜 Step-by-Step 解説

1バギングの分散低減を数値で確認

# バギングのシミュレーション(分散低減の確認)
np.random.seed(42)
# 1つの木は高分散(ノイズに過敏)
# n本の平均では分散が 1/n に減少(平均の法則)
n_trees = [1, 10, 50, 100]
for n in n_trees:
    # 各木が独立なら Var(平均) = Var(1木) / n
    variance_reduction = 1 / n
    print(f"木の数 {n:3d}: 分散は単木の {variance_reduction:.3f} 倍")
# → 木が独立なほど効果大(そのためmax_featuresで木を「バラバラ」にする)

2OOBサンプルの割合を計算

# ブートストラップでN件抽出したとき、特定のサンプルが選ばれない確率
# P(選ばれない) = (1 - 1/N)^N → e^(-1) ≈ 0.368 (N→∞)
import numpy as np
N = 891
prob_not_selected = (1 - 1/N) ** N
print(f"OOBサンプルの割合(理論値): {prob_not_selected:.4f}")  # ≈ 0.3679
print(f"実際のOOBサンプル数の期待値: {int(N * prob_not_selected)}件")

# 確認: 実際に sklearn でOOBサンプルを確認
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=1, oob_score=True, random_state=42)
rf.fit(X, y)
# 各木でOOBサンプルのインデックスを確認できる(内部属性)
print(f"実際のOOBスコア(1木): {rf.oob_score_:.4f}")

3各木の重要度のばらつきを観察

from sklearn.ensemble import RandomForestClassifier
import numpy as np

rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

# 各木の重要度を取得
tree_importances = np.array([tree.feature_importances_ for tree in rf.estimators_])
# shape: (100, 5) = 100本の木 × 5つの特徴量

print("各特徴量の重要度の std(木間のばらつき):")
for f, mean, std in zip(features, tree_importances.mean(axis=0), tree_importances.std(axis=0)):
    print(f"  {f:10s}: 平均={mean:.4f}, std=±{std:.4f}")

print("\n→ 100本の平均を取ることでstdが低下し安定した重要度が得られる")

4Permutation Importance(より信頼性の高い重要度)

from sklearn.inspection import permutation_importance

rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

# Permutation Importance: 特徴量をシャッフルしたときの精度低下
perm_imp = permutation_importance(rf, X, y, n_repeats=10, random_state=42)
print("Permutation Importance(より信頼性が高い):")
for f, mean, std in sorted(
    zip(features, perm_imp.importances_mean, perm_imp.importances_std),
    key=lambda x: -x[1]
):
    print(f"  {f:10s}: {mean:.4f} ± {std:.4f}")
# feature_importances_より信頼性が高いが計算コストが大きい
# → 連続値特徴量の重要度が過大評価される問題を修正できる

5Kaggle標準テンプレート

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_predict, StratifiedKFold
from sklearn.metrics import accuracy_score

# Kaggle 標準パターン
rf = RandomForestClassifier(
    n_estimators=500,       # 多いほど安定(計算コストと相談)
    max_features='sqrt',    # 各分岐で sqrt(n_features) の特徴量を使用
    max_depth=None,         # 制限なし(バギングで過学習を抑制)
    min_samples_leaf=1,     # デフォルト
    oob_score=True,         # OOBスコアで素早く評価
    n_jobs=-1,              # 全CPUコアを使用(並列化)
    random_state=42
)

rf.fit(X, y)
print(f"OOBスコア: {rf.oob_score_:.4f}")  # CVなしで評価

# OOF予測(Kaggle提出前の最終チェック)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof = cross_val_predict(rf, X, y, cv=cv)
print(f"OOF精度: {accuracy_score(y, oof):.4f}")

📐 数学・統計の補足(文系向け)

バギングの分散低減(試験の例え)

100人が「合否」を予測するとします。

  • 1人が外れる確率: 40%
  • 100人の多数決が外れる確率: 大幅に低下(大数の法則)

数式: Var(平均) = Var(1木) / n(木が完全に独立な場合)

実際には木間に相関があるため: Var(RF) = ρ·σ² + (1-ρ)/n·σ²(ρ=木間の相関)

ρ(木間の相関)が小さいほど RFの分散が低下する

木間の相関 ρ(0 = 完全独立, 1 = 全て同じ) 分散 単木の分散 RF の分散 max_features='sqrt'
(低 ρ = 多様性大) max_features=None
(高 ρ = 多様性低)

なぜ max_depth=None でも過学習しないのか

手法max_depth=None の結果理由
決定木(1本)過学習(訓練精度 100%)全データを完全に記憶
ランダムフォレスト適切な汎化各木が異なるデータで学習し、丸暗記がキャンセルされる

🏆 Kaggleでの実践的な使い方

場面使い方備考
ベースライン構築RFをまず試す前処理最小限でもそこそこ動く
特徴量選択feature_importances_で上位を選ぶEDAの後段として有効
ハイパラ調整Optuna + OOBスコアCV不要で高速チューニング
アンサンブルGBDTとのスタッキングRF単体より多くの場合高スコア
欠損値処理RF は NaN 非対応事前に fillna が必要
# Kaggle での RF ベースライン テンプレート
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.metrics import roc_auc_score

rf = RandomForestClassifier(
    n_estimators=500,
    max_features='sqrt',
    oob_score=True,
    n_jobs=-1,
    random_state=42
)

# OOBで素早く評価
rf.fit(X_train, y_train)
print(f"OOBスコア (AUCではなく精度): {rf.oob_score_:.4f}")

# 特徴量の絞り込み(重要度上位50%のみ使用)
importances = rf.feature_importances_
threshold = np.median(importances)
selected_features = [f for f, imp in zip(feature_names, importances) if imp >= threshold]
print(f"選択された特徴量: {len(selected_features)} / {len(feature_names)}")

🚫 よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
n_estimators を増やし続ける 「多いほど良い」という思い込み 100〜500 で収束。以降は計算コストのみ増加
max_depth=None は危険と思う 決定木の知識を誤適用 バギングで過学習が抑制されるため制限不要(むしろ None が標準)
feature_importances を因果と思う 相関と因果の混同 相関が強い特徴量間で重要度が分散する(多重共線性の影響)
oob_score と CV精度が全く同じと思う 原理を誤解 近い値だが乱数・データ分割の違いで差がある
n_jobs=-1 を使わない 並列化を知らない 全コアを使えば大幅に高速化(4コアで約3倍速)

🚀 次のステップ

  • 発展: Permutation Importance と feature_importances_ の違いを深掘り。n_jobs=-1 の速度比較も試す
  • 次回予告(Day 061): 勾配ブースティング入門 — XGBoost の仕組みと基本パラメータ(ランダムフォレストとの比較)

📋 自己評価(解いた後に記入)

✍️

理解度: [ ] 完全理解   [ ] おおむね理解   [ ] 要復習

自分の回答:

気づき・メモ: