Machine Learning: 決定木とランダムフォレスト — 高精度な分類・回帰のためのアンサンブル思考
最終更新:2026-08-26
決定木は「質問の木」のようなものです。各ノードが1つの質問を投げかけ、その答えに沿って葉まで下っていくと、そこに予測結果が待っています。
1. 学習内容
- 決定木の基礎:情報利得/利得比/ジニ不純度、そして ID3/C4.5/CART アルゴリズムの系統
- 木の剪定:事前剪定(max_depth/min_samples_leaf)と事後剪定
- ランダムフォレスト:バギングの考え方、ランダムな特徴量選択、OOB推定
- 特徴量重要度の分析:不純度減少量(MDI)と順列重要度の比較
- Bob による商品カテゴリの評価:ランダムフォレストを使って商品がヒットするかどうかを予測する
2. ECサイトの商品マネージャーの実話
(1) 課題:ヒット商品の予測は完全に勘頼みで、的中率は30%未満
Bob は1000個の新商品の中からヒット候補を選び出す必要がありました。これまでは完全に勘に頼っており、的中率はわずか30%でした。ヒット商品は1つあたり平均で月間50万ドルの売上をもたらすため、1つ逃すだけでも大きな損失になります。勘は定量化できず、再利用もできず、改善もできません。
(2) ランダムフォレストによる解決策
ランダムフォレストは、過去の商品データ(価格帯、カテゴリ特性、初週の売上傾向など)からヒット商品のパターンを自動的に発見でき、さらに特徴量重要度のランキングも自然に提供してくれます。
PYTHON
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf.fit(X_train, y_train)
print(f"OOB Accuracy: {rf.oob_score_:.3f}")
print(f"Top feature: {feature_names[rf.feature_importances_.argmax()]}")
(3) 成果:ヒット商品予測の精度が30%から65%に向上
Bob は勘をランダムフォレストに置き換えることで、ヒット商品予測の精度を30%から65%に引き上げました。これにより毎月5つのヒット商品を余分に特定できるようになり、年間約300万ドルの売上増加につながっています。
3. 決定木の仕組み
(1) 分割基準
各ノードにおいて、決定木は分割に最適な特徴量と閾値を選びます。よく使われる基準は3つあります。
graph TB
ROOT[Root Node<br/>All Data] --> SPLIT1{Feature: price<br/>Threshold: 50 USD}
SPLIT1 -->|≤ 50| LEFT[Left Child<br/>60% hit products]
SPLIT1 -->|> 50| RIGHT[Right Child<br/>10% hit products]
LEFT --> SPLIT2{Feature: first_week_sales}
SPLIT2 -->|> 500| LEAF1[Leaf: HIT<br/>90% confidence]
SPLIT2 -->|≤ 500| LEAF2[Leaf: NOT HIT<br/>40% confidence]
RIGHT --> LEAF3[Leaf: NOT HIT<br/>5% confidence]
| 基準 | 核心となる式 | 傾向 | アルゴリズム系統 |
|---|---|---|---|
| 情報利得 | $H(D) - H(D | A)$ | 多値の特徴量を好む |
| 利得比 | 情報利得/固有値 | 多値バイアスを補正 | C4.5 |
| ジニ不純度 | $1 - \sum p_i^2$ | 大きいクラスの純度を好む | CART |
▶ サンプル:Irisでの決定木による分類
PYTHON
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.3, random_state=42, stratify=iris.target
)
# Train with pre-pruning
tree = DecisionTreeClassifier(max_depth=3, min_samples_leaf=5, random_state=42)
tree.fit(X_train, y_train)
y_pred = tree.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"Tree depth: {tree.get_depth()}")
print(f"Number of leaves: {tree.get_n_leaves()}")
# Visualize tree
fig, ax = plt.subplots(figsize=(14, 8))
plot_tree(tree, feature_names=iris.feature_names,
class_names=iris.target_names, filled=True, rounded=True, ax=ax)
plt.title("Decision Tree (max_depth=3)")
plt.tight_layout()
plt.savefig("decision_tree.png", dpi=150)
出力:
TEXT
📖 参照専用
# Runs successfully
(2) 剪定戦略
▶ サンプル:事前剪定パラメータの比較
PYTHON
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
configs = {
"No pruning": DecisionTreeClassifier(random_state=42),
"max_depth=2": DecisionTreeClassifier(max_depth=2, random_state=42),
"max_depth=3": DecisionTreeClassifier(max_depth=3, random_state=42),
"min_samples_leaf=5": DecisionTreeClassifier(min_samples_leaf=5, random_state=42),
"max_depth=3 + min_samples_leaf=5": DecisionTreeClassifier(
max_depth=3, min_samples_leaf=5, random_state=42),
}
for name, model in configs.items():
scores = cross_val_score(model, X, y, cv=5, scoring="accuracy")
model.fit(X, y)
print(f"{name:35s}: Accuracy={scores.mean():.3f}, Leaves={model.get_n_leaves()}")
出力:
TEXT
📖 参照専用
# Runs successfully
| 剪定の種類 | パラメータ | 効果 | 推奨値 |
|---|---|---|---|
| 事前剪定 | max_depth | 木の深さを制限 | 3-10 |
| 事前剪定 | min_samples_leaf | 葉あたりの最小サンプル数 | 5-20 |
| 事前剪定 | min_samples_split | 分割に必要な最小サンプル数 | 10-40 |
| 事前剪定 | max_features | 分割ごとに検討する特徴量数 | sqrt(n_features) |
| 事後剪定 | ccp_alpha | コスト複雑度剪定 | GridSearchで選択 |
4. ランダムフォレスト
(1) バギングの考え方
ランダムフォレスト = バギング(Bootstrap Aggregating)+ ランダムな特徴量選択。
graph TB
DATA[Original Data] --> B1[Bootstrap Sample 1]
DATA --> B2[Bootstrap Sample 2]
DATA --> B3[Bootstrap Sample 3]
DATA --> BN[Bootstrap Sample N]
B1 --> T1[Tree 1<br/>Random Feature Subset]
B2 --> T2[Tree 2<br/>Random Feature Subset]
B3 --> T3[Tree 3<br/>Random Feature Subset]
BN --> TN[Tree N<br/>Random Feature Subset]
T1 --> VOTE[Majority Vote<br/>/ Average]
T2 --> VOTE
T3 --> VOTE
TN --> VOTE
▶ サンプル:ランダムフォレストによる分類+OOB評価
PYTHON
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
rf = RandomForestClassifier(
n_estimators=100,
max_depth=None,
oob_score=True,
random_state=42,
)
rf.fit(X_train, y_train)
print(f"OOB Score: {rf.oob_score_:.4f}")
print(f"Test Accuracy: {accuracy_score(y_test, rf.predict(X_test)):.4f}")
print(f"Number of trees: {rf.n_estimators}")
出力:
TEXT
📖 参照専用
# Runs successfully
(2) ハイパーパラメータのチューニング
▶ サンプル:ランダムフォレストによる回帰 — SalesPredict の売上予測
PYTHON
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import mean_absolute_error, r2_score
import numpy as np
rng = np.random.default_rng(42)
n = 500
X = rng.uniform(0, 100, (n, 6))
y = 50 + 0.8 * X[:, 0] + 1.2 * X[:, 1] - 0.5 * X[:, 2] + rng.normal(0, 5, n)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Compare different n_estimators
for n_est in [10, 50, 100, 200]:
rf = RandomForestRegressor(n_estimators=n_est, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
score = rf.score(X_test, y_test)
mae = mean_absolute_error(y_test, rf.predict(X_test))
print(f"n_estimators={n_est:3d}: R²={score:.4f}, MAE={mae:.2f}")
出力:
TEXT
📖 参照専用
# Runs successfully
5. 特徴量重要度の分析
(1) 不純度減少量(MDI)と順列重要度の比較
▶ サンプル:2つの特徴量重要度手法の比較
PYTHON
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
import numpy as np
X, y = load_iris(return_X_y=True)
feature_names = load_iris().feature_names
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
# Method 1: MDI (built-in, fast but biased)
mdi_importance = rf.feature_importances_
# Method 2: Permutation importance (slower but more reliable)
perm_result = permutation_importance(rf, X, y, n_repeats=30, random_state=42, n_jobs=-1)
perm_importance = perm_result.importances_mean
# Compare
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
y_pos = np.arange(len(feature_names))
axes[0].barh(y_pos, mdi_importance, color="#2196F3")
axes[0].set_yticks(y_pos)
axes[0].set_yticklabels(feature_names)
axes[0].set_title("MDI Feature Importance")
axes[1].barh(y_pos, perm_importance, color="#4CAF50")
axes[1].set_yticks(y_pos)
axes[1].set_yticklabels(feature_names)
axes[1].set_title("Permutation Feature Importance")
plt.tight_layout()
plt.savefig("feature_importance.png", dpi=150)
出力:
TEXT
📖 参照専用
# Runs successfully
| 観点 | MDI重要度 | 順列重要度 |
|---|---|---|
| 計算速度 | 高速(学習中に計算される) | 低速(繰り返し予測が必要) |
| バイアス | 高カーディナリティの特徴量に偏る | バイアスなし |
| 適用範囲 | ツリーモデルに組み込み済み | 任意のモデルに適用可能 |
| 信頼性 | 中程度 | より信頼性が高い |
▶ サンプル:Bob のヒット商品の特徴量ランキング
PYTHON
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 1000
df = pd.DataFrame({
"price_usd": rng.uniform(10, 200, n),
"first_week_sales": rng.integers(10, 2000, n),
"category_trend_score": rng.uniform(0, 1, n),
"ad_budget_k": rng.uniform(1, 50, n),
"review_score": rng.uniform(1, 5, n),
"return_rate": rng.uniform(0, 0.3, n),
"stock_depth": rng.integers(10, 500, n),
})
df["is_hit"] = (
(df["first_week_sales"] > 500)
& (df["category_trend_score"] > 0.6)
& (df["price_usd"] < 100)
).astype(int) | (rng.random(n) < 0.05) # Add 5% noise
X = df.drop(columns=["is_hit"])
y = df["is_hit"]
rf = RandomForestClassifier(n_estimators=200, random_state=42)
rf.fit(X, y)
importance = pd.DataFrame({
"feature": X.columns,
"importance": rf.feature_importances_,
}).sort_values("importance", ascending=False)
print("Feature Importance for Hit Product Prediction:")
print(importance.to_string(index=False))
出力:
TEXT
📖 参照専用
Feature Importance for Hit Product Prediction:
❓ よくある質問
Q ランダムフォレストはなぜ単一の決定木より優れているのですか?
A 理由は2つあります。1)バギングが分散を低減する(複数の木が投票・平均化する)こと、2)ランダムな特徴量選択が木同士の相関を下げ、アンサンブル全体をより強力にすることです。単一の木は過学習に陥りがちです。
Q n_estimators は大きければ大きいほど良いのですか?
A 必ずしもそうではありません。ある程度を超えると効果が頭打ちになり、計算時間だけが増えてしまいます。通常は100〜500で十分です。OOBスコアが安定するポイントを確認しましょう。
Q OOBスコアとは何ですか?
A Out-of-Bag(アウトオブバッグ)評価のことです。各木はデータの約63%で学習され、残りの37%が自然に検証セットとして機能します。OOBスコアは、すべての木が自分自身のOOBサンプルに対して行った予測の平均であり、交差検証と同等の役割を果たしますが、追加コストはかかりません。
Q 決定木に標準化は必要ですか?
A 不要です。決定木は特徴量の閾値で分割するため、スケールの影響を受けません。ただし、ランダムフォレストを他のモデル(Pipeline内のScalerなど)と組み合わせる場合は、必要になることがあります。
Q すべての特徴量重要度が0に近い場合はどうすればよいですか?
A その特徴量が目的変数と実際に関係が薄い可能性があります。特徴量の交互作用や非線形変換を試すか、順列重要度を使ってMDIにバイアスがかかっていないか確認してみましょう。
Q ランダムフォレストはクラス不均衡を扱えますか?
A はい。class_weight="balanced" または class_weight={0:1, 1:10} を設定してください。オーバーサンプリング(SMOTE)やアンダーサンプリングと組み合わせることもできます。
📖 まとめ
- 決定木は分割基準(情報利得/ジニ不純度)を使って再帰的に構築され、停止条件を満たすまで分割が続きます
- 剪定は過学習を防ぎます:事前剪定(深さ/葉のサンプル数の制限)+ 事後剪定(ccp_alpha)
- ランダムフォレスト = バギング + ランダムな特徴量選択であり、分散を低減して過学習に強くします
- OOBスコアはランダムフォレストの「無料の」交差検証であり、訓練/テスト分割による評価を置き換えられます
- 特徴量重要度の手法は2つ:MDI(高速だがバイアスあり)と順列重要度(低速だが信頼性が高い)
- ランダムフォレストは機械学習ツールボックスの「万能ナイフ」のような存在で、ほぼ常に堅実なベースライン選択肢となります
📝 練習問題
- 基礎(難易度 ⭐):DecisionTreeClassifier を使って Iris を分類し、max_depth を 1 から 5 まで変化させながら、精度と深さの関係を表す曲線をプロットしてください。ヒント:
cross_val_scoreを使って学習をループさせます。 - 応用(難易度 ⭐⭐):RandomForestRegressor を California Housing で使い、n_estimators=[10,50,100,200] それぞれの R² と OOBスコアを比較してください。ヒント:
oob_score=Trueを設定します。 - 挑戦(難易度 ⭐⭐⭐):Bob のヒット商品予測パイプライン全体を実装してください。シミュレーションデータの生成、RandomForestClassifier の学習、permutation_importance による特徴量分析、class_weight による不均衡の処理(ヒット商品は10%未満)、分類レポートの出力を行います。ヒント:第5章のヒット商品の例を参照してください。