論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説(ジャストインタイム型データサイエンス教育)
決定木・アンサンブル学習
Decision Trees & Ensemble Learning
弱い学習器を組み合わせて強い学習器を作る — テーブルデータ最強の伝統
教師あり学習分類・回帰両用テーブルデータ向け解釈性中

🔖 キーワード索引 (拡張)

🔖 キーワード索引 (拡張)

Random Forest GBDT XGBoost LightGBM CatBoost Bagging Boosting Stacking SHAP Feature Importance Gini Importance Permutation Importance Bootstrap m_try 学習率 early stopping

🔖 🔖 キーワード索引(チップから該当箇所へジャンプ)

論文記事から各用語のリンクをクリックすると、 該当箇所が開きます:

決定木 ジニ不純度 情報エントロピー 情報利得 剪定 アンサンブル学習 バギング ランダムフォレスト 特徴量重要度 OOB誤差 ブースティング AdaBoost 勾配ブースティング XGBoost LightGBM CatBoost スタッキング SHAP

💡 30秒で分かる結論

🍰 まずはやさしく

YesかNoで分ける仕組みです。

正しい答えを導き出すために使います。

買い物で商品を選ぶときのような判断です。

この章では決定木と学習の方法を学びます。

💡 30 秒で分かる結論 (拡張)

📍 あなたが今見ているもの

🍰 まずはやさしく

弱い力を合わせて強くする方法です。

予測のミスを減らすために使います。

部活でみんなの意見をまとめる感覚です。

ここでは複数のモデルを組む方法を読みます。

本ページは、 決定木を出発点として、 バギング・ランダムフォレスト・ブースティング(AdaBoost / 勾配ブースティング / XGBoost / LightGBM / CatBoost)・スタッキングまでを一気通貫で解説する統合ページです。

これらは「弱い学習器を多数組み合わせて強い学習器を作る」というアンサンブル学習の発想に基づきます。 SSDSE-B のような中規模テーブルデータでは、 深層学習よりこれらが第一選択となることが多いです。

📍 文脈ボックス: あなたが今見ているもの

本ページの「ツリーアンサンブル」は 機械学習における集成学習 (ensemble learning) の決定木版。 単一木が抱える「過学習しやすい・分散が大きい」弱点を、 複数木の組み合わせで克服する手法群を指します。

🎨 直感で掴む: ツリーアンサンブルを「審査員の合議制」で理解する

🍰 まずはやさしく

審査員による多数決のようなものです。

一人だけの判断による偏りをなくします。

クラスの出し物をみんなで決める例です。

ここでは合議制の仕組みを直感的に学びます。

単一の決定木は「1 人の審査員」に過ぎません。 ツリーアンサンブルは「100 人の審査員」を集めて多数決 (分類) や平均 (回帰) を取る合議制。 個々の審査員は偏っていても、 多数決により偏りが相殺されて全体精度が向上します。

手法学習方法直感的説明代表ライブラリ
Random ForestBagging (並列)独立な審査員 100 人が同時に意見、 多数決sklearn
GBDTBoosting (逐次)前審査員の誤りを次が修正、 リレー方式sklearn, lightgbm
XGBoostBoosting + 正則化GBDT に L1/L2 + 並列化、 Kaggle 標準xgboost
LightGBMBoosting + leaf-wiseGBDT に leaf-wise tree、 大規模データで高速lightgbm
CatBoostBoosting + Ordered TSGBDT にカテゴリ変数のリーク防止catboost
Isolation ForestBagging (異常検知)「変な例ほど早く孤立する」性質を利用sklearn

🎨 ツリーアンサンブル vs ニューラルネット (使い分け表)

条件推奨手法理由
表形式データ (テーブル)GBDT (XGBoost/LightGBM)2024 年の比較でも依然 SOTA
画像CNN/Vision Transformer空間構造の学習はツリーでは困難
テキストTransformer (BERT/GPT)系列依存・文脈学習が必要
特徴量数 100 以下RF or GBDTDL は学習データ不足になりやすい
特徴量数 1000 以上DL or 線形 + L1ツリーは高次元で速度問題
解釈性が必須RF + SHAPdecision path を辿れる
サンプル数 100 以下RF or LRDL は過学習しやすい

🎨 ツリーアンサンブルの歴史と発展 (年表)

年出来事提案者
1984CART (決定木) の提案Breiman et al.
1996Bagging の提案Breiman
1996AdaBoostFreund & Schapire
2001Random Forest 命名Breiman
2001Gradient Boosting MachineFriedman
2014XGBoost 公開Chen & Guestrin
2017LightGBM 公開Microsoft
2017CatBoost 公開Yandex
2017SHAP (TreeExplainer)Lundberg & Lee
2022XGBoost 2.0 (multi-output, GPU)DMLC
2024表形式 DL モデル群との比較で依然 SOTAGrinsztajn et al.

2024 年現在、 表形式データに関しては GBDT が依然として SOTA を維持。 DL モデル (TabNet、 SAINT、 FT-Transformer) も近づいているが、 小〜中規模では GBDT が優位。

🎨 概念図で押さえる木アンサンブルの本質(補遺)

決定木のアンサンブル(バギング / ランダムフォレスト / ブースティング)の中核を 3 枚の図で再整理する。 並列の多数決と、 逐次の誤差補正、 その境界を視覚化する。

🖼 図 1: 単一決定木 vs ランダムフォレスト

単一の決定木は学習データに過剰適合しやすく、 境界がギザギザに崩れる。 多くの木をブートストラップで作って平均すれば、 境界は 滑らかになり汎化性能が大きく改善する。

単一木とランダムフォレストの境界比較

💬 読み方:左の赤線(単一木)は階段状でデータの境目をなぞるため、 ノイズに敏感。 右の緑線(RF)は多数の木の 多数決 なので、 個々の木の不安定さがキャンセルされ、 滑らかで頑健な境界になる。

🖼 図 2: バギング(並列)vs ブースティング(逐次)

2 大アンサンブル流派の根本的な違い。 バギングは ブートストラップ標本で並列学習し平均、 ブースティングは 前の木の残差を次の木が補正する逐次学習。 計算の流れ自体が異なる。

バギングとブースティングのフロー

💬 読み方:左(青)の バギングは元データから複数の bootstrap 標本 → 並列に深い木を学習 → 平均。 右(オレンジ)のブースティングは 浅い木 を逐次的に積み、 各木は前の木の残差を補正する。 並列化のしやすさはバギング、 精度の極限はブースティング。

🖼 図 3: Bias-Variance 軸上のアンサンブル位置

アンサンブルの効用は 「分散低減(バギング)」 と 「バイアス低減(ブースティング)」の 2 軸で整理できる。 単一木はどちらも改善余地が大きく、 用途で使い分ける。

アンサンブルとバイアス・バリアンスの位置

💬 読み方:単一決定木(右上, 赤)はバイアス・バリアンスとも大きい。 バギング/RF(左上, 緑)は バリアンスを下げる、 ブースティング(左下, オレンジ)は バイアスを下げる。 切り株(弱学習器)は左下に位置し、 単独では使われずブースティングの素材になる。

以上、 木アンサンブルの本質(多数決の安定化・残差補正の逐次学習・バイアス vs バリアンス)を 3 枚の図で整理した。 詳細は本文参照。

🔎 補足: 木アンサンブルの実務的判断基準

ランダムフォレストと勾配ブースティングをいつ使い分けるか

ランダムフォレスト (RF) と勾配ブースティング (XGBoost / LightGBM / CatBoost) は、 多くのコンペで「最初に試すべき 2 大候補」です。 使い分けの目安は次の通り。 RF は (1) ハイパラ調整が比較的緩い (n_estimators と max_features の 2 軸が主)、 (2) 並列学習が容易で計算が速い、 (3) 過学習しにくい、 (4) 特徴量重要度の解釈が安定、 という強みを持ちます。 一方、 勾配ブースティングは (1) ピーク性能が高い (Kaggle 上位入賞の主役)、 (2) 学習率と木の深さ・反復回数の組み合わせ調整が必要、 (3) 早期停止 (early stopping) が必須、 (4) カテゴリ特徴の扱いに長ける、 という特徴があります。 SSDSE-B-2026 の都道府県データ (47 件) なら、 まず RF で大枠を掴み、 ピーク性能を狙うときに XGBoost で詰める、 という二段構えが安全です。

OOB (Out-Of-Bag) スコアの活用

ランダムフォレストの隠れた利点が OOB スコアです。 各木はブートストラップサンプル(重複あり抽出)で学習しますが、 1 つの木から見ると約 37% のサンプルが「学習に使われなかった」サンプルになります。 これらを使って自動的に検証スコアを計算できるので、 別途 cross-validation を組まなくても汎化性能の推定値が手に入ります。 scikit-learn では RandomForestRegressor(oob_score=True) と指定するだけで、 学習後に rf.oob_score_ から R² を取得できます。 SSDSE-B-2026 のような小規模データでは「CV を組むと 1 fold あたり 9〜10 件しか残らない」問題があるので、 OOB スコアは特に役立ちます。

特徴量重要度の 3 種類と落とし穴

木モデルの特徴量重要度には主に 3 種類あります。 (1) Gini / impurity-based: scikit-learn の feature_importances_ で得られる既定値。 計算が速いが、 高カーディナリティ特徴量に過大評価されるバイアスがある。 (2) Permutation importance: 特徴量をランダムに並べ替えてスコア低下を測る。 バイアスがないが計算が重い。 (3) SHAP value: 各サンプル・各特徴量の寄与を加法的に分解する。 解釈性が高くサンプルごとの説明も可能だが、 さらに重い。 SSDSE-B-2026 で「人口を予測するモデルの重要特徴量を知りたい」場合、 最低でも permutation importance か SHAP を使い、 Gini ベースは参考程度にとどめるのが鉄則です。 「Gini ベースで重要と出た特徴量が、 permutation importance では無関係」という現象は実務で頻繁に起きます。

カテゴリカル特徴量の扱い

木モデルは数値特徴量を扱うことを基本としますが、 都道府県名のようなカテゴリカル特徴量を入れたいことがあります。 選択肢は (1) one-hot encoding、 (2) ordinal encoding、 (3) target encoding (out-of-fold で計算)、 (4) CatBoost のネイティブカテゴリサポート、 (5) LightGBM の categorical_feature 指定、 の 5 つ。 カーディナリティが低い(カテゴリ数 10 未満)なら one-hot で十分ですが、 47 都道府県のように中規模カーディナリティでは target encoding か LightGBM の native 対応が効率的です。 ordinal encoding は順序情報を持つカテゴリ(小・中・大など)に限定すべきで、 都道府県のような名義変数には不適切です。

過学習を防ぐ 5 つのレバー

木アンサンブルの過学習を防ぐには、 5 つのハイパラを総合的に調整します。 (1) max_depth: 木の深さ上限。 浅くするほど過学習しにくい (RF は 10〜20、 GBDT は 3〜8 が定石)。 (2) min_samples_leaf: 葉の最少サンプル数。 大きくすると過学習しにくい。 (3) subsample: 各木で使うサンプル比率。 1.0 未満にすると確率的勾配ブースティング (SGB) になり過学習耐性が増す。 (4) colsample_bytree / max_features: 各木で使う特徴量の比率。 RF では √p、 GBDT では 0.5〜0.8 が標準。 (5) n_estimators + early stopping: ブースティングでは反復回数を大きく取り、 検証スコアが悪化したら止める。 SSDSE-B-2026 で過学習に悩んだら、 まず max_depth を下げ、 次に min_samples_leaf を上げ、 最後に subsample を 0.8 程度に絞る、 の順で試すと迷子になりません。

木モデルが苦手なケース

木アンサンブルは万能ではありません。 苦手なのは (1) 高次元疎データ (テキスト・画像など)、 (2) 滑らかな関数を厳密に近似したいケース (木は階段関数なので極端な外挿が苦手)、 (3) 時系列の外挿予測 (訓練データ範囲外を予測できない)、 (4) 連続的に変化する変数間の交互作用 (深いネットワークの方が表現力が高い)、 の 4 つです。 SSDSE-B-2026 で「2030 年の高齢化率を予測する」タスクでは、 木モデルだけでは過去のトレンドの外挿ができないので、 ARIMA や Prophet と組み合わせるのが現実的です。

📝 理解度チェック

  1. バギングとブースティングがそれぞれ低減するのは何か。 (答:バギング=分散、 ブースティング=バイアス)
  2. ランダムフォレストの OOB スコアとは何か。 (答:各木のブートストラップサンプル外データを使った検証スコア。 別途 CV を組まなくても汎化性能を推定できる)
  3. Gini ベース重要度の問題点は? (答:高カーディナリティ特徴量を過大評価する。 permutation importance か SHAP で補完すべき)
  4. 勾配ブースティングで early stopping が必須なのはなぜ? (答:反復を増やすほど過学習が進むため、 検証スコアの悪化点で停止する必要がある)
  5. 木モデルが苦手なタスクを 3 つ挙げよ。 (答:高次元疎データ、 訓練範囲外の外挿、 滑らかな関数近似)
  6. SSDSE-B-2026 のようにサンプル数が少ない場合に RF vs GBDT のどちらから試すべきか。 (答:RF。 ハイパラ調整が緩く OOB スコアで簡単に汎化性能が見られるため)

6 問中 5 問以上正解で「木アンサンブルの使い分けと運用を理解した」と自己評価できる。 5 問未満なら「使い分け」「特徴量重要度」「過学習を防ぐレバー」の節を再読する。

🔎 補足: 主要木アンサンブル実装の比較

XGBoost / LightGBM / CatBoost の使い分け

勾配ブースティング系の 3 大実装には次の特徴があります。 XGBoost (Chen & Guestrin 2016): 最初に世に出た高性能実装。 細かい正則化が効き、 GPU 対応も成熟。 中規模データで広く採用される。 LightGBM (Microsoft 2017): ヒストグラム分割と leaf-wise 成長で XGBoost より概ね 5〜10 倍高速。 SSDSE-B-2026 のような小規模だと差は出にくいが、 数十万行を超える Kaggle データでは事実上の標準。 CatBoost (Yandex 2017): カテゴリ特徴量のネイティブサポートが秀逸。 ターゲットエンコーディングを内部で out-of-fold 計算するためリーク耐性が高く、 デフォルトハイパラの完成度が高い。 ハイパラ調整に時間を掛けたくない実務案件で重宝されます。 47 都道府県程度なら 3 つとも数秒で学習が終わるので、 教育目的では「同じデータで 3 つを動かしてスコアと SHAP 値を比較」を実習にすると学びが深まります。

ハイパラ調整の優先順位

勾配ブースティングのハイパラは 20 個以上ありますが、 影響が大きいものから順に攻めるのが鉄則です。 (1) learning_rate (0.01〜0.3): 小さくすると安定だが n_estimators を増やす必要がある。 デフォルト 0.1 が出発点。 (2) max_depth (3〜10) または num_leaves (LightGBM): 木の容量を制御。 浅すぎると underfit、 深すぎると overfit。 (3) n_estimators: early stopping で自動決定するのが普通。 上限 10000 程度を設定し、 50 round 改善しなければ停止。 (4) min_child_weight / min_data_in_leaf: 葉のサンプル数下限。 過学習を防ぐ重要レバー。 (5) subsample / colsample_bytree: 確率的サンプリングで過学習耐性を上げる。 0.7〜0.9 を試す。 (6) reg_lambda / reg_alpha: L2/L1 正則化。 最後の微調整。 Optuna でこの順に Bayesian 探索を組むと、 SSDSE-B-2026 で 30 試行ほどで最適点近傍に到達します。

解釈手法: SHAP の活用

勾配ブースティングのブラックボックス性を解消するのが SHAP (SHapley Additive exPlanations) です。 木モデル専用の高速計算 (TreeSHAP) があり、 SSDSE-B-2026 程度なら 1 秒以内に全サンプルの寄与分解が終わります。 SHAP の使い道は (1) 全体重要度: shap.summary_plot(shap_values, X) で「どの特徴量が予測にどう寄与するか」を可視化、 (2) 個別予測の説明: shap.force_plot(...) で「東京都の高齢化率予測がなぜ 28% になったか」を加法分解、 (3) 相互作用: shap.dependence_plot('人口', shap_values, X) で「人口の影響が高齢化率によって変わるか」を発見、 の 3 点です。 提案資料に SHAP の図を 1 枚入れるだけで「ブラックボックスではない」と信頼を得られます。

残差分析でモデル改善ヒントを得る

木モデルの予測残差を散布図にすると、 系統的バイアスが見えます。 横軸に予測値、 縦軸に残差を取って散布図を描き、 (1) 帯状の偏りがあれば「外れ値を別モデルで扱うべき」、 (2) 漏斗形なら「対数変換などで分散を等しくすべき」、 (3) 特定範囲だけ残差が大きいなら「その範囲で特徴量を増やすべき」、 などの改善方針が見えます。 SSDSE-B-2026 で人口を予測すると、 東京都・神奈川・大阪府の残差が大きく出ることが多く、 「大都市指標」のような特徴量を追加すると劇的に改善するケースがあります。 残差分析は線形回帰の専売特許ではなく、 木モデルでも欠かせない診断手段です。

スタッキングとブレンディングへの発展

木アンサンブルの先には「異種モデルを組み合わせる」スタッキング (stacking) があります。 第 1 層に RF・XGBoost・LightGBM・線形モデル・ニューラルネットを並列で学習させ、 それぞれの out-of-fold 予測値を特徴量として第 2 層 (メタモデル) に渡す方式です。 Kaggle 上位解の常套手段で、 単一モデルより 1〜3% スコアが伸びるケースが多いです。 SSDSE-B-2026 では「RF + Ridge 回帰 + LightGBM をスタッキングし、 メタモデルに Lasso を採用」のような構成を試すと、 単一 LightGBM より 0.5〜1% 改善することがあります。 計算コストは増えますが、 教育目的では「異種モデルを組み合わせる発想」を実体験する価値があります。

木モデルの不確実性推定: Quantile Regression Forest

通常の木モデルは点予測しか出しませんが、 Quantile Regression Forest (QRF) を使うと予測の信頼区間を出せます。 QRF は学習時の各葉に観測値の分布を保存しておき、 推論時に「予測値 ± 信頼区間」を出力します。 SSDSE-B-2026 で「秋田県の 2030 年高齢化率は 39.2% ± [37.5%, 41.0%]」のような区間予測ができ、 政策判断に直結する不確実性情報を提供できます。 Python では quantile-forest パッケージや scikit-learn の GradientBoostingRegressor(loss='quantile', alpha=0.9) で実装できます。

🧾 発表前の最終確認

木アンサンブルでは、 単木数、深さ、学習率、サブサンプリング、特徴量重要度の種類を明示します。 重要度は予測への寄与を示す手がかりであり、 因果効果と混同しないよう注意します。

🎮 触って理解する: バギング(並列に平均)vs ブースティング(逐次に誤り訂正)

同じ「浅い回帰木」を材料にしても、 組み合わせ方が違うとアンサンブルの性格は一変します。 下のデモは、 非線形な 1 次元回帰データ(真の関数 $f(x)=\sin 2\pi x$ +ノイズ、 46 点)に対して、 (A) バギング=ブートストラップ標本ごとに独立に木を学習して平均、 (B) ブースティング=前段までの残差に木を逐次フィットして加算、 を同一条件で切り替えて観察できます。 木の計算は近似ではなく、 CART と同じ「二乗誤差を最小化する厳密な貪欲分割」で行っています。

方式:

💡 グラフ上を左右にドラッグ(スマホはスワイプ)しても木の本数 M を変えられます。

方式
バギング
訓練 MSE(ノイズ込みデータへの当てはまり)
—
真の関数との RMSE(汎化の目安)
—
残差の平均絶対値 |y−ŷ|
—

🧭 何を観察すればよいか(実験レシピ)

  1. バギング × M を 1→50:1 本目はギザギザ(分散大)だが、 平均する木が増えるほど緑の太線が滑らかに安定していく。 ただし訓練 MSE はある水準(個々の木のバイアス)以下には下がらない。 バギングは分散を減らすが、 バイアスは減らせない。
  2. バギング × 深さ 1(決定株):50 本平均しても階段 1 段の平均にしかならず、 sin カーブを表現できない。 弱すぎる学習器の平均は弱いまま — RF が「ある程度深い木」を使う理由がここにあります。
  3. ブースティング × 深さ 1 × M を 1→50:決定株の和なのに、 段階的に残差(下段の縦棒)が縮み、 sin カーブに肉薄していく。 ブースティングは弱い木を足し合わせてバイアスを減らす。 下段の残差プロットが「次の木が学ぶ教材」です。
  4. 学習率 ν の効果:ν=1.0 にすると少ない本数で訓練 MSE が急落するが、 「真の関数との RMSE」は途中から悪化に転じる(ノイズまで拾う過学習)。 ν=0.1 では本数が多く必要な代わりに、 なだらかに最適点へ近づき過学習しにくい。 これが 学習率と本数のトレードオフ(shrinkage の正則化効果)。
  5. 2 つの指標の乖離:訓練 MSE は単調に下がり続けても、 真の関数との RMSE が上がり始めたら過学習のサイン。 実務ではこの「真の関数」が見えないので、 交差検証や early stopping で代用します。

🎨 直感の核心: 並列に平均 vs 逐次に誤り訂正

観点バギング(RF の原理)ブースティング(GBDT の原理)
木同士の関係独立(ブートストラップ標本が違うだけ)。 順番を入れ替えても結果は同じ依存(木 m は木 1〜m−1 の残差を学ぶ)。 順番に意味がある
結合方法平均 $\hat f = \frac{1}{M}\sum_m T_m(x)$加重和 $F_M = F_0 + \nu\sum_m h_m(x)$
減らすもの分散(ノイズ由来のブレ)バイアス(モデルの表現力不足)
ベース木の推奨深め(低バイアス・高分散な木を平均で鎮める)浅め(高バイアス・低分散な木を積んで補う)
M を増やしすぎると性能は頭打ちになるだけで悪化しにくい訓練誤差 0 に向かい過学習する(early stopping 必須)
並列化容易(木ごとに独立計算)木単位では不可(分割探索内部のみ並列化)

⚠️ このデモで見えるよくある落とし穴

🚀 発展: このデモと実装ライブラリの対応

※ デモのデータは動作説明用に生成した合成データ(乱数シード固定)であり、 SSDSE-B-2026 の実測値ではありません。 実データでの数値例は本ページ「🧮 SSDSE-B-2026 で実値計算」の章を参照してください。

📐 数式と定義: ツリーアンサンブルの中核 5 式

🍰 まずはやさしく

計算で答えを出すためのルールです。

予測の精度を正しく測るために使います。

スマホのアプリが計算する仕組みに似ています。

ここでは中核となる数式について読みます。

① Bagging (Bootstrap Aggregating)

$$\\hat{f}_{\\mathrm{bag}}(x) = \\frac{1}{M} \\sum_{m=1}^{M} \\hat{f}_m(x)$$

🔬 数式を言葉で読み解く:M 個のブートストラップ標本でそれぞれ木 \\(\\hat{f}_m\\) を学習し、 出力を平均 (回帰) または多数決 (分類)。 分散が \\(\\sigma^2 / M\\) に下がる → 過学習が緩和される。

② Random Forest の特徴量サブサンプリング

$$\\text{各ノード分割: } m_{\\mathrm{try}} = \\sqrt{p} \\text{ (分類)}, \\quad m_{\\mathrm{try}} = p/3 \\text{ (回帰)}$$

🔬 数式を言葉で読み解く:各ノードで全 p 特徴量から \\(m_{\\mathrm{try}}\\) 個だけランダム選択。 木同士の相関を下げて bagging の効果を最大化。 これが RF が「単純 bagging より優れる」核心理由。

③ Gradient Boosting (GBDT) の更新式

$$F_t(x) = F_{t-1}(x) + \\eta \\cdot h_t(x), \\quad h_t = \\arg\\min_h \\sum_i L(y_i, F_{t-1}(x_i) + h(x_i))$$

🔬 数式を言葉で読み解く:第 t 段で前段の予測 \\(F_{t-1}\\) に学習率 \\(\\eta\\) (0.01〜0.1) 倍した新木 \\(h_t\\) を加算。 \\(h_t\\) は損失 L の負の勾配 (residual) を fit する。 学習率を小さくすると過学習しにくいが時間がかかる。

④ XGBoost の正則化付き損失

$$\\mathcal{L}(\\phi) = \\sum_i L(y_i, \\hat{y}_i) + \\sum_t \\Omega(f_t), \\quad \\Omega(f) = \\gamma T + \\frac{1}{2} \\lambda \\| w \\|^2$$

🔬 数式を言葉で読み解く:木の葉数 T に \\(\\gamma\\)、 葉の重み \\(w\\) に L2 ノルム \\(\\lambda\\) で正則化。 GBDT に「木の複雑さ」penalty を入れたものが XGBoost の本質。

⑤ Feature Importance (gini importance)

$$\\mathrm{FI}_j = \\frac{1}{M} \\sum_{m=1}^{M} \\sum_{\\text{node using } j} \\Delta \\mathrm{Gini}$$

🔬 数式を言葉で読み解く:特徴量 j が分割に使われたノードでの Gini 減少量を全木で平均。 「この特徴量が無いとどれだけ精度が落ちるか」の代理指標。 ただしカテゴリ数が多いとバイアスがかかる (Permutation Importance 推奨)。

📐 アンサンブル学習が分散を下げる根拠 (数理)

バギングが「なぜ精度を上げるか」を分散減少の観点から数理的に説明します。

$$\\mathrm{Var}\\left[\\frac{1}{M} \\sum_{m=1}^{M} \\hat{f}_m(x)\\right] = \\frac{1-\\rho}{M} \\sigma^2 + \\rho \\sigma^2$$

🔬 数式を言葉で読み解く:M 個の予測値を平均すると、 木同士が完全独立 (ρ=0) なら分散は \\(\\sigma^2 / M\\) に減少。 しかし実際には木同士に相関 ρ があり、 ρσ² の項が残る。 Random Forest が「ノードで特徴量サブサンプル」する理由は、 この ρ を下げて分散減少効果を最大化するため。

🐍 ρ の効果をシミュレーション

🎯 このコードでやること:M=100 木で相関 ρ を変化させたとき、 アンサンブル分散がどう変化するかを可視化。

📥 入力: M=100、 ρ ∈ {0, 0.1, 0.3, 0.5, 0.8}、 σ²=1。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pandas as pd

M = 100
sigma2 = 1.0
rhos = [0.0, 0.1, 0.3, 0.5, 0.8]
rows = []
for rho in rhos:
    var_ensemble = (1 - rho) / M * sigma2 + rho * sigma2
    var_single   = sigma2
    reduction    = 1 - var_ensemble / var_single
    rows.append({'rho': rho, 'var_ensemble': var_ensemble,
                 'reduction_%': reduction * 100})
print(pd.DataFrame(rows).round(4).to_string(index=False))

📤 実行結果:

rho var_ensemble reduction_% 0.0 0.010 99.0 0.1 0.109 89.1 0.3 0.307 69.3 0.5 0.505 49.5 0.8 0.802 19.8

💬 結果の読み方:ρ=0 なら 99% 分散減少、 ρ=0.5 なら半分にしか減らない、 ρ=0.8 なら 20% しか減らない。 Random Forest が「ノードで特徴量サブサンプル」する設計思想がここに直結している。

📐 ハイパラ最適化: Optuna で GBDT を 30 trials

🎯 このコードでやること:SSDSE-B-2026 上で Optuna による GBDT ハイパラ最適化。 30 trials で最適 (max_depth, learning_rate, n_estimators) を探索。

📥 入力: SSDSE-B-2026 (47 県)、 探索空間 = max_depth∈[2,8], lr∈[0.01,0.3], n_est∈[50,500]。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pandas as pd
import optuna
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)
def to_class(pop):
    if pop >= 5_000_000: return 'L'
    if pop >= 1_000_000: return 'M'
    return 'S'
df['cls'] = df['A1101'].apply(to_class)
features = ['A4101', 'A4200', 'B4101', 'B4106', 'B4109', 'C3301']
X = df[features].fillna(df[features].median())
y = df['cls']

def objective(trial):
    depth = trial.suggest_int('max_depth', 2, 8)
    lr    = trial.suggest_float('learning_rate', 0.01, 0.3, log=True)
    n_est = trial.suggest_int('n_estimators', 50, 500)
    gb = GradientBoostingClassifier(
        max_depth=depth, learning_rate=lr, n_estimators=n_est, random_state=42
    )
    return cross_val_score(gb, X, y, cv=5).mean()

## 種を固定して、 誰が実行しても同じ結果になるようにする
study = optuna.create_study(direction='maximize',
                            sampler=optuna.samplers.TPESampler(seed=0))
study.optimize(objective, n_trials=30, show_progress_bar=False)

print(f'Best score: {study.best_value:.3f}')
print(f'Best params: {study.best_params}')

📤 実行結果:

Best score: 0.916 Best params: {'max_depth': 3, 'learning_rate': 0.017160616005703237, 'n_estimators': 184}

💬 結果の読み方:既定パラメータのままの 5 分割 CV が 0.896 なのに対し、 30 trials 後は 0.916 (+2.0 pt)。 最適は「浅い木 (depth=3) + 小さめの学習率 0.017 + n_estimators=184」で、 ツリー系のハイパラ最適化の典型パターン。 <code>TPESampler(seed=0)</code> で種を固定してあるため、 同じ手順を踏めば誰がやっても上と同じ値が出る。

🔬 数式を言葉で読み解く

数式の記号・要素を言葉で説明する。

前節の数式に含まれる記号を、 日本語の意味に翻訳する。

tree ensemble の数式は、 これらの記号を組み合わせて「データから未知量を推定する」あるいは「データの構造を要約する」プロセスを記述している。

🧮 SSDSE-B-2026 で実値計算: 47 県を Random Forest で分類

SSDSE-B-2026 の 47 県データから「人口規模カテゴリ (大/中/小)」を予測する分類問題を Random Forest で解きます。

🎯 このコードでやること:SSDSE-B-2026 を読み込み、 総人口を 3 クラス (大: 500 万人超、 中: 100〜500 万、 小: 100 万未満) に分け、 出生数・死亡数・気温・降水・着工建築物数の 8 列から RF で分類。 feature_importances_ で寄与度を確認。

📥 入力データ (SSDSE-B-2026 抜粋、 主要列):

都道府県 総人口(A1101) 出生数(A4101) 死亡数(A4200) 年平均気温(B4101) 降水量(B4109) 着工建築物数(C3301) 北海道 5,092,000 24,430 75,120 11.0 966.0 15,872 青森県 1,184,000 5,696 20,835 12.6 1316.0 5,043 東京都 14,086,000 86,348 137,241 17.6 1396.5 41,817 ... (全 47 県。総人口はクラス分けにだけ使い、説明変数には入れない)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

# SSDSE-B-2026 を読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)

# 総人口を 3 クラスに離散化
def to_class(pop):
    if pop >= 5_000_000: return 'L'  # 大
    if pop >= 1_000_000: return 'M'  # 中
    return 'S'                       # 小
df['cls'] = df['A1101'].apply(to_class)

# 説明変数: 出生数・死亡数・気温・降水・着工建築物
features = ['A4101', 'A4200', 'B4101', 'B4102', 'B4103', 'B4106', 'B4109', 'C3301']
X = df[features].fillna(df[features].median())
y = df['cls']

# Random Forest
rf = RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42)
scores = cross_val_score(rf, X, y, cv=5)
print(f'5-fold CV accuracy: {scores.mean():.3f} ± {scores.std():.3f}')

rf.fit(X, y)
importance = pd.Series(rf.feature_importances_, index=features).sort_values(ascending=False)
print('\nFeature Importance:')
print(importance.round(3))

📤 実行結果:

5-fold CV accuracy: 0.938 ± 0.081 Feature Importance: C3301 0.298 # 着工建築物数 (人口の代理指標) A4200 0.293 # 死亡数 A4101 0.263 # 出生数 B4106 0.038 # 年間降水日数 B4101 0.034 # 年平均気温 B4103 0.029 # 最低気温 B4109 0.025 # 年間降水量 B4102 0.020 # 最高気温

💬 結果の読み方:2023 年の 47 県のみ(年度リークなし)で 5-fold CV 約 94% 精度。 最重要特徴量は 着工建築物数 (C3301)・死亡数 (A4200)・出生数 (A4101) — いずれも人口規模と強く連動する量。 気候要因(気温・降水)の寄与は小さい。 RF はこのように「どの特徴が効くか」を可視化できる強み。

GBDT (LightGBM) と精度比較

🎯 このコードでやること:同じ問題を sklearn の GradientBoostingClassifier で解き、 RF と精度・速度を比較。

📥 入力: 上記と同じ X, y。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
import time

gb = GradientBoostingClassifier(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42)
start = time.time()
scores_gb = cross_val_score(gb, X, y, cv=5)
gb_time = time.time() - start

start = time.time()
scores_rf = cross_val_score(rf, X, y, cv=5)
rf_time = time.time() - start

print(f'RF:  acc {scores_rf.mean():.3f} ± {scores_rf.std():.3f}   time {rf_time:.2f}s')
print(f'GBM: acc {scores_gb.mean():.3f} ± {scores_gb.std():.3f}   time {gb_time:.2f}s')

📤 実行結果:

RF: acc 0.938 ± 0.081 time 0.28s GBM: acc 0.916 ± 0.076 time 0.31s

💬 結果の読み方:2023 年 47 県では RF と GBDT はほぼ同水準(RF 0.938 / GBM 0.916)で、 47 件の小規模データでは 1 fold(9〜10 県)で 1 県の差が約 10 ポイントになるので、 0.022 の差は誤差の範囲。 学習時間も 0.28 秒と 0.31 秒でほぼ同じで、 実行環境によって前後する。 大規模データ (10 万行以上) では LightGBM の leaf-wise growth が顕著に高速。

🧮 SSDSE-B-2026 で過学習を可視化: max_depth の効果

RF の max_depth を 2〜20 まで動かし、 訓練精度・検証精度の差から過学習を検出します。

🎯 このコードでやること:max_depth を 2〜20 まで変化させ、 train_score と test_score の差を観察。 過学習の境界 (max_depth) を可視化。

📥 入力: SSDSE-B-2026 (47 県)、 目的変数 = 人口 3 クラス。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)
def to_class(pop):
    if pop >= 5_000_000: return 'L'
    if pop >= 1_000_000: return 'M'
    return 'S'
df['cls'] = df['A1101'].apply(to_class)
features = ['A4101', 'A4200', 'B4101', 'B4106', 'B4109', 'C3301']
X = df[features].fillna(df[features].median())
y = df['cls']

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)

results = []
for depth in [2, 3, 5, 7, 10, 15, 20]:
    rf = RandomForestClassifier(n_estimators=100, max_depth=depth, random_state=42)
    rf.fit(X_tr, y_tr)
    results.append({
        'max_depth': depth,
        'train_acc': rf.score(X_tr, y_tr),
        'test_acc':  rf.score(X_te, y_te),
        'gap':       rf.score(X_tr, y_tr) - rf.score(X_te, y_te),
    })
print(pd.DataFrame(results).round(3).to_string(index=False))

📤 実行結果:

max_depth train_acc test_acc gap 2 1.000 1.000 0.000 3 1.000 1.000 0.000 5 1.000 1.000 0.000 7 1.000 1.000 0.000 10 1.000 1.000 0.000 15 1.000 1.000 0.000 20 1.000 1.000 0.000

💬 結果の読み方:2023 年 47 県のみに絞ると、 人口規模クラス(大/中/小)は着工建築物数・出生数・死亡数からほぼ完全に線形分離できてしまい、 どの深さでも train=test=100%、 gap=0 になる。 つまりこのターゲットは易しすぎて過学習が観測されない。

📝 より正確な分析(教材補足): 年度で絞らずに全 564 行(2012〜2023 の複数年度)を混ぜて学習すると、 同じ県の別年度が train と test に分かれる年跨ぎリークが生じ、 train/test gap も年度混在ノイズを映してしまう。 2023 年 47 県に正しく絞ると人口規模 3 クラスはこの特徴量セットで自明に分離でき、 過学習の「gap 拡大」現象は現れない。 過学習を体感したい場合は、 ノイズの多い連続ターゲット(例: 消費支出 L3221 の回帰、 後掲の GBDT 回帰例を参照)や、 分離しにくいクラス定義を使うとよい。

🧮 SHAP で 47 県分類の説明

予測結果を「どの特徴がどれだけ寄与したか」を SHAP 値で説明します。 個別予測の解釈に強い。

🎯 このコードでやること:RandomForest(3 クラス分類)の予測に対し SHAP TreeExplainer を適用し、 特徴量の寄与度を数値で比較。

📥 入力: SSDSE-B-2026 (2023 年 47 県) の 6 変数と、 学習済み RandomForestClassifier (3 クラス分類)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
import pandas as pd
import shap
from sklearn.ensemble import RandomForestClassifier

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)
def to_class(pop):
    if pop >= 5_000_000: return 'L'
    if pop >= 1_000_000: return 'M'
    return 'S'
df['cls'] = df['A1101'].apply(to_class)
features = ['A4101', 'A4200', 'B4101', 'B4106', 'B4109', 'C3301']
X = df[features].fillna(df[features].median())
y = df['cls']

gb = RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42)
gb.fit(X, y)

# SHAP 値計算 (TreeExplainer はRF/決定木アンサンブルに対応)
explainer = shap.TreeExplainer(gb)
shap_values = np.asarray(explainer.shap_values(X))
print('shap_values.shape =', shap_values.shape)  # (47 サンプル, 6 特徴, 3 クラス)

# 全サンプル平均寄与度(多クラスはサンプル軸とクラス軸の両方を平均する)
axes = (0, 2) if shap_values.ndim == 3 else 0
mean_abs_shap = pd.Series(
    np.abs(shap_values).mean(axis=axes), index=features
).sort_values(ascending=False)
print('SHAP 平均寄与度:')
print(mean_abs_shap.round(4))

📤 実行結果:

shap_values.shape = (47, 6, 3) SHAP 平均寄与度: A4101 0.1209 # 出生数 A4200 0.1109 # 死亡数 C3301 0.1076 # 着工建築物数 B4101 0.0099 # 平均気温 B4109 0.0082 # 降水量 B4106 0.0069 # 降水日数

💬 結果の読み方:2023 年 47 県で RandomForest に shap.TreeExplainer を適用した実測値。 出生数・死亡数・着工建築物数の 3 つが人口規模クラスの判別に効き、 気候変数の寄与は 1 桁小さい。 Gini Importance と概ね一致するが、 SHAP は 個別予測ごとの寄与も分解できる点が強み。 なお sklearn の多クラス GradientBoostingClassifier は TreeExplainer 非対応のため、 ここでは RF を用いている。 ブラックボックス批判への強力な回答。

🧮 数式に値を入れて手で計算する: アンサンブル多数決

合成 5 木の予測から多数決を計算する。

Step 1: 個別予測

サンプルt1t2t3t4t5多数決
s1110111
s2010010
s3101101

Step 2: 投票結果

s1: 4/5=1 s2: 2/5=0 s3: 3/5=1

🐍 Python で再現

1
2
3
4
import numpy as np
preds = np.array([[1,1,0,1,1],[0,1,0,0,1],[1,0,1,1,0]])
vote = (preds.mean(axis=1) > 0.5).astype(int)
print(f"多数決: {vote}")

📤 実行結果

多数決: [1 0 1]

💬 手計算 (Step 2) と Python 出力が完全一致。

🐍 Python 実装バリエーション(scikit-learn / xgboost / lightgbm / catboost)

同じ「高齢化率の高低(中央値で二値化)」の分類を 4 つのライブラリで実装する例。 API の違いと共通点が分かります。

A. scikit-learn の HistGradientBoosting(依存なし・最速ベースライン)

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) A4200(死亡数) B4101(年平均気温) 北海道 5,092,000 1,681,000 24,430 75,120 11.0 東京都 14,086,000 3,205,000 86,348 137,241 17.6 沖縄県 1,468,000 350,000 12,549 15,110 23.8 …(全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
import lightgbm as lgb

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)
rate = df['A1303'] / df['A1101']  # 高齢化率 = 65歳以上人口 / 総人口
df['y'] = (rate >= rate.median()).astype(int)
X = df[['A4101', 'A4200', 'B4101', 'L3221']]
y = df['y']

for name, model in [
    ('Tree', DecisionTreeClassifier(max_depth=5, random_state=42)),
    ('RF',   RandomForestClassifier(n_estimators=500, random_state=42)),
    ('LGB',  lgb.LGBMClassifier(n_estimators=500, learning_rate=0.05, verbose=-1, n_jobs=1,
                             min_child_samples=3)),  # 既定 20 では 37 件の訓練で分岐できない
]:
    sc = cross_val_score(model, X, y, cv=5, scoring='accuracy')
    print(f'{name}: {sc.mean():.3f} ± {sc.std():.3f}')
📤 実行例(実測) Tree: 0.684 ± 0.124 RF: 0.727 ± 0.134 LGB: 0.664 ± 0.128

💬 高齢化率が中央値以上かの 2 値(47 県中 24 県が 1)なので、全部を多い方に当てるだけで約 0.51 になる。RF 0.727 が最も高く、単一の木 0.684 と LightGBM 0.664 がそれに続くが、5 分割の各 fold は 9〜10 県しかなく ±0.12〜0.13 の揺れがあるので、3 手法の差は誤差の範囲に収まる。LightGBM は葉の最小件数を既定の 20 のまま使うと 37 件の訓練ではほとんど分岐できず、0.467 と当て推量以下に落ちていた。

Q2. Random Forest で特徴量重要度を出し、 さらに Permutation Importance と比較しなさい。
1
2
3
4
5
6
7
8
9
from sklearn.inspection import permutation_importance
rf.fit(X, y)
perm = permutation_importance(rf, X, y, n_repeats=30, random_state=42, n_jobs=-1)
import pandas as pd
result = pd.DataFrame({
    'MDI':  rf.feature_importances_,
    'Perm': perm.importances_mean,
}, index=X.columns).sort_values('Perm', ascending=False)
print(result)
📤 実行例(実測) MDI Perm A4101 0.434825 0.381560 B4101 0.186212 0.090780 A4200 0.249928 0.058156 L3221 0.129035 0.053901

💬 ここでの rf は前の深さ比較ループで最後に作った max_depth=20・100 本の森で、訓練に使った 47 県そのものに対して並べ替え重要度を測っている。どちらの指標でも A4101(出生数)が首位で、Perm 0.382 は「出生数をシャッフルすると正解率が約 38 ポイント落ちる」という意味。MDI では A4200(死亡数)が 0.250 で 2 位なのに Perm では 0.058 と 3 位に下がり、出生数と相関の強い死亡数に分割の手柄が分散していたことが分かる。汎化性能に効く特徴を知りたいなら、検証データで測り直す。

Q3. LightGBM で early stopping を使い、 訓練・検証損失曲線をプロットしなさい。
1
2
3
4
5
6
7
8
9
10
11
12
13
import lightgbm as lgb
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
# 訓練 32 件なので葉の最小件数を 3 に下げる(既定 20 だと 1 本も分岐できず損失が 0.693 のまま)
model = lgb.LGBMClassifier(n_estimators=1000, learning_rate=0.05, verbose=-1,
                           min_child_samples=3)
model.fit(X_tr, y_tr, eval_set=[(X_tr, y_tr), (X_va, y_va)],
          eval_metric='binary_logloss',
          callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)])
lgb.plot_metric(model)
plt.show()
📤 実行例(実測) Training until validation scores don't improve for 50 rounds Early stopping, best iteration is: [20] training's binary_logloss: 0.373117 valid_1's binary_logloss: 0.530088

💬 訓練 32 県・検証 15 県に分け、検証の logloss が 50 ラウンド改善しなかったので 20 本目の木で止まった。そのとき訓練 0.373・検証 0.530 で、差の 0.157 が過学習の始まりを示す。どちらも初期値 0.693(= ln 2、確率 0.5 を出し続けたときの損失)から下がっているので、木が実際に分岐していることも確認できる。min_child_samples を既定 20 のままにすると損失が 0.693147 から 1 ミリも動かず 1 本目で止まっていた。

📝 12. 報告フォーマット

❌ NG例

「Random Forest を使った結果、 accuracy 0.85 でした。」

✅ OK例

「LightGBM(n_estimators=500, learning_rate=0.05, early_stopping_rounds=50)で 5-fold Stratified CV を行い、 macro-F1 = 0.78 ± 0.03 を得た。 ベースラインのロジスティック回帰(0.71)に対し +0.07。 特徴量重要度(Permutation)では『出生数』が最も寄与し、 SHAP 値による個別解釈でも同じ傾向を確認した。」

🐍 13. ライブラリ早見表

手法 主要パッケージ・クラス 主要引数
決定木sklearn.tree.DecisionTree{Classifier,Regressor}criterion, max_depth, min_samples_leaf, ccp_alpha
Random Forestsklearn.ensemble.RandomForest{Classifier,Regressor}n_estimators, max_features, oob_score, class_weight
Extra Treessklearn.ensemble.ExtraTrees{Classifier,Regressor}RFと同じ + 分岐閾値ランダム
AdaBoostsklearn.ensemble.AdaBoostClassifiern_estimators, learning_rate, algorithm
GBMsklearn.ensemble.GradientBoosting{Classifier,Regressor}n_estimators, learning_rate, max_depth, subsample
HistGBMsklearn.ensemble.HistGradientBoosting{Classifier,Regressor}LightGBM相当の高速実装
XGBoostxgboost.XGB{Classifier,Regressor}n_estimators, eta, max_depth, lambda, alpha, gamma
LightGBMlightgbm.LGBM{Classifier,Regressor}n_estimators, learning_rate, num_leaves, feature_fraction
CatBoostcatboost.CatBoost{Classifier,Regressor}iterations, learning_rate, depth, cat_features
Stackingsklearn.ensemble.StackingClassifierestimators, final_estimator, cv, passthrough
SHAPshap.TreeExplainerツリー系専用の高速 SHAP

📜 14. ツリー系・アンサンブル法の歴史

💼 15. 実務応用例

🐍 GBDT で SSDSE-B-2026 を回帰: 消費支出を予測

🎯 このコードでやること:SSDSE-B-2026 の県別データで、 人口・出生・着工建築物・地価などから 1 世帯あたり消費支出(L3221)を推定する回帰問題を勾配ブースティングで解く。

📥 入力データ: SSDSE-B-2026 47 県、 説明変数 = 総人口/年少人口/出生数/死亡数/着工建築物数/着工床面積/地価、 目的変数 = 消費支出(L3221)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import KFold, cross_val_score
import numpy as np

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)

# 目的変数: 消費支出 (L3221)
# 説明変数: 人口・出生・着工・公共設備
features = ['A1101', 'A1301', 'A4101', 'A4200', 'C3301', 'C3302', 'C5401']
df_clean = df[features + ['L3221']].dropna()
X = df_clean[features]
y = df_clean['L3221']

# GBDT 回帰
gbr = GradientBoostingRegressor(
    n_estimators=300, max_depth=3, learning_rate=0.05, random_state=42
)
cv = KFold(n_splits=5, shuffle=True, random_state=0)
r2_scores = cross_val_score(gbr, X, y, cv=cv, scoring='r2')
neg_mse  = cross_val_score(gbr, X, y, cv=cv, scoring='neg_mean_squared_error')
rmse = np.sqrt(-neg_mse).mean()
print(f'5-fold CV R^2 = {r2_scores.mean():.3f} ± {r2_scores.std():.3f}')
print(f'5-fold CV RMSE = {rmse:,.0f} 円/月')

gbr.fit(X, y)
imp = pd.Series(gbr.feature_importances_, index=features).sort_values(ascending=False)
print('\nFeature Importance:')
print(imp.round(3))

📤 実行結果:

5-fold CV R^2 = -1.128 ± 0.423 5-fold CV RMSE = 33,057 円/月 Feature Importance: C3301 0.510 # 着工建築物数 A4101 0.172 # 出生数 A4200 0.107 # 死亡数 C5401 0.070 # 標準価格 (住宅地) A1101 0.064 # 総人口 C3302 0.051 # 着工建築物床面積 A1301 0.027 # 15歳未満人口

💬 結果の読み方:2023 年 47 県のみで正しく評価すると CV R² = -1.128(負!)── 平均値予測にすら負けており、 このモデルは汎化していない。 特徴量 7 個・サンプル 47 件では GBDT(n_estimators=300)が容易に過学習し、 fold 外の県をまったく当てられない。 訓練データ上の feature_importances_(C3301 が最大)は「見かけの寄与」に過ぎない。

📝 より正確な分析(教材補足): 年度で絞らずに全 564 行(12 年度分)を混ぜて学習すると R² が高めに出て「良好」に見えてしまうが、 これは年度リークによるもの ── 同じ県の別年度がほぼ同値のため train/test に分かれても実質的に答えを見ているに等しい。 本来評価すべき「未知の県を当てる」設定(2023 年 47 県のみ)では R²=-1.128 と破綻する。 これは「小サンプル × 高分散モデル × リークで水増しされた過去スコア」という失敗パターンの典型例で、 GBDT を 47 件で使うなら特徴量削減・強い正則化(max_depth=1〜2、 学習率↓、 n_estimators↓)や線形モデルとの比較が必須。 消費支出予測そのものが 47 県では難しいことを示す好例でもある。

🐍 Stacking で 3 モデル統合 (実践例)

🎯 このコードでやること:RF・GBDT・LR の 3 モデルを stacking で統合し、 単一モデルとの精度比較。 SSDSE-B-2026 47 県の人口クラス分類が題材。

📥 入力: SSDSE-B-2026 (47 県)、 特徴量 8 列、 目的変数 = 人口 3 クラス。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import pandas as pd
from sklearn.ensemble import (
    RandomForestClassifier, GradientBoostingClassifier, StackingClassifier
)
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)
def to_class(pop):
    if pop >= 5_000_000: return 'L'
    if pop >= 1_000_000: return 'M'
    return 'S'
df['cls'] = df['A1101'].apply(to_class)
features = ['A4101', 'A4200', 'B4101', 'B4102', 'B4103', 'B4106', 'B4109', 'C3301']
X = df[features].fillna(df[features].median())
y = df['cls']

# 個別モデル
rf = RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42)
gb = GradientBoostingClassifier(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42)
lr = Pipeline([('sc', StandardScaler()), ('lr', LogisticRegression(max_iter=1000))])

# Stacking: 3 base + LR meta
stack = StackingClassifier(
    estimators=[('rf', rf), ('gb', gb), ('lr', lr)],
    final_estimator=LogisticRegression(max_iter=1000),
    cv=5
)

for name, mdl in [('RF', rf), ('GB', gb), ('LR', lr), ('Stack', stack)]:
    sc = cross_val_score(mdl, X, y, cv=5)
    print(f'{name:6s}: {sc.mean():.3f} ± {sc.std():.3f}')

📤 実行結果:

RF : 0.938 ± 0.081 GB : 0.916 ± 0.076 LR : 0.727 ± 0.099 Stack : 0.916 ± 0.076

💬 結果の読み方:2023 年 47 県では単独 RF が 0.938 と最も高く、 Stack (0.916) は RF を上回らなかった。 線形モデル (LR 0.727) が弱いターゲットでは、 スタッキングが必ずしも単独ベストを超えるとは限らない ── 特にサンプルが 47 件と少ないと meta 学習の恩恵が出にくい。 多様性のあるモデルを混ぜる手法自体は Kaggle 上位解の常套手段だが、 「必ず勝つ」わけではない点に注意。

🐍 Isolation Forest で 47 県の異常検知

🎯 このコードでやること:SSDSE-B-2026 の 47 県の中で「他県と乖離した県」を Isolation Forest で発見。 異常スコアで上位 3 県を抽出。

📥 入力: SSDSE-B-2026 (47 県)、 特徴量 = 人口・出生・気温・降水・消費。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import pandas as pd
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]  # 年度列で最新年に絞る (B は 2012〜2023 の複数年度を含む)
features = ['A1101', 'A4101', 'B4101', 'B4109', 'L3221']  # 人口/出生/気温/降水/消費
X = df[features].fillna(df[features].median())
X_sc = StandardScaler().fit_transform(X)

iso = IsolationForest(contamination=0.1, random_state=42)
df['anomaly_score'] = -iso.fit(X_sc).score_samples(X_sc)
top3 = df.nlargest(3, 'anomaly_score')[['Prefecture', 'A1101', 'anomaly_score']]
print('異常スコア上位 3 県:')
print(top3.to_string(index=False))

📤 実行結果:

異常スコア上位 3 県: Prefecture A1101 anomaly_score 東京都 14086000 0.687182 沖縄県 1468000 0.597820 北海道 5092000 0.565477

💬 結果の読み方:人口・出生数・消費支出が突出する東京、 気温・降水量が全国から大きく乖離する沖縄、 大きな人口と低い気温を併せ持つ北海道が「異常」と判定される (2023 年値、 実測)。 これは「データ的に異常」というだけで「悪い」わけではない (= 特徴量空間での外れ値)。 異常検知は「警告フラグ」であり、 「アクション」とは独立に解釈する。

⚠️ 10. よくある落とし穴

落とし穴 対処
1本の木を信用しすぎるアンサンブルで分散低減。 訓練 data の僅かな違いで木は大きく変わる。
特徴量重要度を絶対視MDI バイアス。 Permutation Importance / SHAP を併用。
外挿(範囲外予測)ツリーは外挿できない。 訓練データの範囲外は予測しないかドメイン知識で補正。
ブースティングの早期停止忘れearly_stopping_rounds を必ず設定。 過学習防止と高速化に必須。
時系列データに通常CVTimeSeriesSplit を使用。 リークを避ける。
クラス不均衡scale_pos_weight / class_weight / is_unbalance を設定。
前処理過剰ツリー系はスケーリング不要。 単調変換でも結果は不変。

⚠️ 詳説:落とし穴 10 連発(各 100 文字以上の詳細解説)

1. 「1本の決定木の解釈」をそのまま現実のロジックだと信じてしまう。決定木は訓練データの微小な揺らぎ(行の1〜2件入れ替え)で分岐構造が大幅に変わる「高分散モデル」です。 同じ問題でランダム種を変えると、 ルート分岐の特徴量自体が入れ替わることも珍しくありません。 解釈用には Random Forest / GBM の SHAP・Permutation Importance で頑健性を確認してから語るのが安全です。

2. feature_importances_ (MDIベース)を変数間で単純比較する。MDI は「分岐に何回使われたか」を反映するため、 カテゴリ水準数や連続値の細かさが大きい変数ほど膨張する既知バイアスを持ちます。 ID 列やゼロ膨張ダミーを混ぜると、 ノイズが上位に来ます。 Permutation Importance(テストセット)や SHAP の平均絶対値で再検証する習慣を持ちましょう。

3. 訓練データの範囲外を予測させる(外挿)。決定木・Random Forest・GBM は本質的に区分定数関数なので、 訓練データに無かった領域(極端な最小値より小さい、 最大値より大きい)では、 直近の葉ノードの定数を返すだけで、 トレンドを延長しません。 時系列の右端予測や、 都道府県別人口の外れ値領域では、 線形モデルや指数族の併用が安全です。

4. early_stopping_rounds を設定し忘れる。LightGBM / XGBoost / CatBoost は n_estimators を大きく取りつつ early stopping で実際の木数を決めるのが標準運用です。 これを怠ると、 巨大な学習率と小さな n_estimators で「学習し切れない」か、 逆に「過学習」のどちらかに必ず転びます。 検証セットを明示的に渡し、 best_iteration を尊重しましょう。

5. 時系列なのに通常の KFold でランダム分割する。SSDSE-A のような時間的構造を持つデータでは、 未来→過去のリークが発生し、 CV 上は超高性能なのに本番運用で性能が出ない現象が頻発します。 TimeSeriesSplit、 GroupKFold(被験者・地域 ID 単位)、 Walk-Forward Validation のいずれかを必ず使ってください。

6. クラス不均衡を無視して accuracy を最適化する。陽性 1% のデータで全件を陰性と予測すると accuracy 99% に達するため、 ツリー系でも勾配計算は陰性に偏ります。 class_weight='balanced'(sklearn)、 scale_pos_weight(XGB)、 is_unbalance=True(LGB)を使うか、 PR-AUC・F1・Cohen κ などの不均衡耐性指標で評価しましょう。

7. ハイパーパラメータをグリッドで全数探索する。木の本数 × 学習率 × 深さ × サブサンプル × 正則化 …と組合せると数万通りに爆発します。 Optuna / Hyperopt のベイズ最適化、 あるいは Random Search 100 回で十分到達可能なケースが多いです。 まず learning_rate と num_leaves/max_depth から探索する優先順位戦略が定石です。

8. データリーク(target leakage)に気付かない。「将来のアウトカム」が特徴量に紛れ込むと、 ツリーは即座にそれを最重要変数として採用し、 CV 性能が異常に高くなります。 完了日・支払日・退院日など「結果と同時に決まる」列、 ID 由来のハッシュ、 後処理で生成された集計値などは慎重に除外しましょう。

9. SHAP の解釈を「介入効果」と混同する。SHAP は「現在のモデルがその予測値に到達するまでにどの特徴量がどれだけ寄与したか」のシャープレイ分解であり、 「この特徴量を変えたら結果はこう変わる」という因果ではありません。 因果が知りたい場合は DiD / IV / RCT などのデザイン的アプローチを併用しましょう。

10. random_state を固定せずに結果を比較する。ブートストラップ・特徴量サブサンプル・初期分岐の揺らぎで、 同じデータでも accuracy が 0.01〜0.03 ぶれます。 モデル間比較や論文用の数値報告では random_state=42 等を固定し、 さらに乱数を変えた 5〜10 回の平均±標準偏差を併記するのが推奨です。

⚠️ 重要な注意点(誤解を避ける)

「特徴量重要度」「SHAP」は予測への寄与であり、 因果効果ではない。

⚠️ ツリーアンサンブルの 5 大落とし穴

  1. 過学習 (overfitting):木の深さ無制限・木数を増やしすぎると訓練データに過適合。 対策:max_depth ≤ 8、 min_samples_leaf ≥ 5、 early_stopping を使う。
  2. 木の深さの罠:深い木は単独で高分散、 浅い木は高バイアス。 GBDT は浅い木 (depth 3-6) を多数、 RF は深い木 (depth 10+) で動かす。
  3. 計算量とメモリ:n_estimators=1000、 max_depth=20 だとメモリ数 GB に達する。 対策:LightGBM や histogram-based GBM で削減。
  4. 解釈性の誤解:Gini importance は カテゴリ数が多い特徴を過大評価。 対策:Permutation Importance / SHAP を併用。
  5. 外挿 (extrapolation) 不能:訓練データの範囲外を予測すると、 木は最近の葉値で打ち止め。 対策:時系列の trend は GBM 単独では捉えにくい → 線形モデルとの組合せ (residual boosting)。

⚠️ ツリーアンサンブルの追加落とし穴 (続編)

  1. カテゴリ変数の高 cardinality 問題:one-hot で爆発、 ラベルエンコードで順序誤解釈。 対策:CatBoost のような category-aware モデル、 または Target Encoding (リーク注意)。
  2. 不均衡データへの過信:少数クラスの精度が低いまま「全体 95%」で満足。 対策:class_weight='balanced'、 SMOTE、 PR-AUC を主指標に。
  3. OOB スコアと CV の混同:RF の OOB スコアは便利だが CV ほど厳密ではない。 対策:本番判断は必ず StratifiedKFold で。
  4. random_state 依存:random_state を変えると精度が ±2% 動くことがある。 対策:複数 seed で平均、 best seed を選ばない (selection bias)。
  5. 木の数を増やしすぎ:n_estimators=10000 にしても accuracy はサチる。 対策:early_stopping_rounds で打ち切る、 検証曲線で頭打ち地点を探す。

🗺 概念マップ

Tree ensemble (Random Forest / Gradient Boosting / XGBoost / LightGBM / CatBoost) を中心に、 単独決定木 (CART) を前提に、 バギング / ブースティング / スタッキングへの派生、 特徴量重要度・SHAP への接続を 6 方向に整理。

決定木・アンサンブル学習 CART / 単独決定木 バギング / ブースティング スタッキング / 特徴量重要度

Tree ensemble の中心は Random Forest・Gradient Boosting・XGBoost・LightGBM・CatBoost の 5 系統。 前提に 単独の決定木 (CART)、 並列に バギング / ブースティング、 発展に スタッキング / ヘテロアンサンブル がある。

SSDSE-B-2026 の「都道府県の人口・出生・着工建築物 → 消費支出(L3221)」を予測する場合、 説明変数を総人口・出生数・死亡数・年平均気温・着工建築物数とした 5 分割 CV(shuffle あり)で、 1 本の決定木の MAE は約 23,800 円、 RF (n_estimators=500) は約 17,700 円と 4 分の 3 ほどに下がる。 これがアンサンブルの「分散低減」効果の実例だが、 全県の平均で予測したときの MAE も約 18,500 円なので、 消費支出は 47 県ではそもそも予測が難しく、 RF でも平均予測をわずかに上回る程度にとどまる。

🔗 隣接手法への橋渡し

Tree ensemble は表形式データの「定番モデル」で、 前処理から評価まで明確なパイプラインがある。

SSDSE-B-2026 (47 県 × 50 指標) のような中小規模データでは LightGBM + 5-fold CV + Early Stopping が標準。 NN より高速で精度も高い。

🌳 4. Random Forest

🌳 4. Random Forest

バギング + 各分岐で特徴量もランダムサンプリング。 木同士の相関が下がり、 平均化効果がさらに高まる。

4.1 主要ハイパーパラメータ

4.2 特徴量重要度

分岐に使った特徴量が平均でどれだけ不純度を下げたかで重要度を算出(mean decrease in impurity)。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) A4200(死亡数) B4101(年平均気温) C3301(着工建築物数) 北海道 5,092,000 24,430 75,120 11.0 15,872 東京都 14,086,000 86,348 137,241 17.6 41,817 沖縄県 1,468,000 12,549 15,110 23.8 5,217 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)
X = df[['A1101', 'A4101', 'A4200', 'B4101', 'C3301']]
y = df['L3221']

rf = RandomForestRegressor(n_estimators=500, max_depth=8, random_state=42, n_jobs=-1)
rf.fit(X, y)

importance = pd.Series(rf.feature_importances_, index=X.columns).sort_values()
importance.plot(kind='barh', color='#43A047')
plt.title('特徴量重要度(RF)')
plt.xlabel('Importance')
plt.tight_layout()
plt.show()
📤 実行例(実測) このブロックは標準出力を出さない(図を描く・変数を定義するだけ)。

注意:MDI バイアス

カテゴリ数の多い・連続変数の特徴量ほど重要度が大きく出る傾向(MDI バイアス)。 より信頼性の高い順列重要度(Permutation Importance)や SHAP を併用するのが実務標準。

🚀 5. ブースティング (Boosting)

弱学習器を直列に並べ、 前の学習器の誤りを次が補正するように学習を進める。

5.1 AdaBoost (Adaptive Boosting, 1995)

誤分類したサンプルの重みを大きくし、 次の弱学習器がそれを優先的に学ぶ。

$$\alpha_m = \frac{1}{2}\log\frac{1-\epsilon_m}{\epsilon_m}, \quad w_i \leftarrow w_i \exp(\alpha_m \cdot \mathbb{1}[y_i \ne \hat{y}_i])$$

記号読み:$\alpha_m$ は「アルファ・サブ・エム」、 第 $m$ 弱学習器の重み。 誤分類率 $\epsilon_m$ が小さい学習器ほど大きな影響を持つ。

5.2 勾配ブースティング (Gradient Boosting Machine)

損失関数 $L$ の負の勾配(残差に相当)に新しい木をフィットさせていく。

$$F_m(x) = F_{m-1}(x) + \nu \cdot h_m(x), \quad h_m \approx -\left.\frac{\partial L(y, F)}{\partial F}\right|_{F=F_{m-1}}$$

記号読み:$F_m$ は「エフ・サブ・エム」、 $m$ 段目までの合計予測。 $\nu$ は「ニュー」と読み、 学習率(0.01〜0.1)。 $h_m$ は新たに加える弱木。

1
2
3
4
5
6
from sklearn.ensemble import GradientBoostingClassifier
gbm = GradientBoostingClassifier(
    n_estimators=500, learning_rate=0.05, max_depth=3, random_state=42
)
gbm.fit(X_train, y_train)
print('Test acc:', gbm.score(X_test, y_test))
📤 実行例(実測) Test acc: 0.7333333333333333

💬 テスト 15 県中 11 県正解で 0.733。木 500 本・学習率 0.05・深さ 3 は標準的な設定だが、訓練 32 件に対しては多すぎ、gbm.score(X_train, y_train) を出すと 1.0 で訓練データを完全に暗記している。訓練 1.0 とテスト 0.733 の差がそのまま過学習の大きさになる。15 県のテストでは 1 県の差が 6.7 ポイントになる点も割り引いて読む。

⚡ 6. XGBoost / LightGBM / CatBoost

6.1 XGBoost (eXtreme Gradient Boosting, 2014)

6.2 LightGBM (2017)

6.3 CatBoost (2018)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 早期終了の判定に使う検証データを、 訓練データからさらに切り出す
X_fit, X_val, y_fit, y_val = train_test_split(
    X_train, y_train, test_size=0.25, random_state=42, stratify=y_train)

train = lgb.Dataset(X_fit, label=y_fit)
val   = lgb.Dataset(X_val, label=y_val, reference=train)

params = dict(
    objective='binary',
    learning_rate=0.05,
    num_leaves=31,
    feature_fraction=0.9,
    bagging_fraction=0.8,
    bagging_freq=5,
    min_data_in_leaf=3,   # 47 県と小さいので既定 20 のままだと木が育たない
    verbose=-1,
)
model = lgb.train(params, train, num_boost_round=1000,
                  valid_sets=[val],
                  callbacks=[lgb.early_stopping(50, verbose=False)])
pred = (model.predict(X_test) >= 0.5).astype(int)
print('best_iteration:', model.best_iteration)
print('LGB Accuracy:', accuracy_score(y_test, pred))
📤 実行例(実測) best_iteration: 25 LGB Accuracy: 0.8

💬 訓練 32 県をさらに 24 県と 8 県に分け、8 県の検証損失で早期終了させた結果、最良は 25 本目。テスト 15 県で 12 県正解の 0.8 で、GradientBoosting の 0.733 より 1 県多く当たった。検証が 8 県しかないので、best_iteration は分割の乱数を変えると大きく動く。本数をこの値に固定して使い回すより、交差検証で平均的な本数を決める方が安定する。

6.4 三者比較

項目 XGBoost LightGBM CatBoost
速度速い最速やや遅
カテゴリ事前エンコード必要対応最強対応
既定値の精度中中高
過学習耐性中過学習しやすい強い
エコシステム豊富豊富中

🥞 7. スタッキング (Stacking)

複数の異質モデル(RF・LGB・ロジスティック等)の予測を、 さらに別のメタモデル(ロジスティック等)が統合する 2 段構成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from sklearn.ensemble import StackingClassifier, RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

base = [
    ('rf',  RandomForestClassifier(n_estimators=300, random_state=42)),
    ('svm', SVC(probability=True, random_state=42)),
]
stack = StackingClassifier(
    estimators=base,
    final_estimator=LogisticRegression(max_iter=1000),
    cv=5, n_jobs=-1,
)
stack.fit(X_train, y_train)
print(stack.score(X_test, y_test))
📤 実行例(実測) 0.8

💬 RF と SVM の予測確率をロジスティック回帰で束ねたスタッキングは、テスト 15 県中 12 県正解の 0.8。LightGBM と同じ点数で、1 県刻みの世界では差がないと見るべき。SVC は標準化していない総人口(100 万人単位)と気温(10℃台)をそのまま受けているので、スケールをそろえる Pipeline を挟むと SVM 側の寄与が変わるかを次に確かめたい。

🔍 8. SHAP — モデルを説明する標準ツール

ツリー系モデルの予測寄与をサンプル単位で分解する。 「なぜこの個体は『高い』と予測されたのか」が分かる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import numpy as np
import pandas as pd
import shap
import lightgbm as lgb

# X_train / X_test は前掲の 3 特徴(A1101 総人口・B4101 年平均気温・L3221 消費支出)
feat_names = ['A1101', 'B4101', 'L3221']
lgb_clf = lgb.LGBMClassifier(n_estimators=200, learning_rate=0.05,
                             num_leaves=15, min_child_samples=3, verbose=-1)
lgb_clf.fit(X_train, y_train)

explainer = shap.TreeExplainer(lgb_clf)
sv = explainer(X_test)                   # Explanation オブジェクトで受け取る
shap.summary_plot(sv, X_test, feature_names=feat_names, show=False)
print('平均 |SHAP|:')
print(pd.Series(np.abs(sv.values).mean(axis=0), index=feat_names).round(4).to_string())
📤 実行例(実測) 平均 |SHAP|: A1101 5.1623 B4101 1.1616 L3221 2.5560

💬 平均 |SHAP| は総人口 A1101 が 5.16 で最大、消費支出 2.56、年平均気温 1.16 の順。LightGBM の 2 値分類では SHAP 値はロジット(対数オッズ)単位なので、5.16 は確率に直すと 0.5 を 0.99 近くまで押し上げる大きさで、訓練 32 件に葉の最小 3 件で 200 本積んだ木がかなり強気の予測をしていることを表す。確率そのものの寄与として読まないこと。

📊 9. 手法横断比較

手法 精度 速度 解釈性 過学習 推奨用途
決定木低〜中速高しやすい説明用途・ルール抽出
Random Forest中〜高中中強い汎用ベースライン
XGBoost / LightGBM高中〜高中CV必須本番モデル(テーブル)
CatBoost高中中既定値で強いカテゴリ多めデータ
Stacking最高遅低CV慎重に最後の伸ばし

🌳 木のアンサンブル: Bagging vs Boosting vs Stacking 徹底比較

木のアンサンブルは「弱学習器(浅い決定木)を多数組み合わせて強学習器を作る」枠組み。 組み合わせ方の違いで Bagging / Boosting / Stacking の 3 系統に分かれる。 SSDSE-B-2026 の都道府県人口予測タスクで比較する。

系統学習方式分散低減バイアス低減代表アルゴリズム
Bagging並列・独立に学習し平均◎△Random Forest, Extra Trees
Boosting直列・残差を逐次フィット○◎XGBoost, LightGBM, CatBoost
Stackingメタ学習器で組み合わせ○○sklearn StackingRegressor

🐍 数式を言葉で読み解く: Random Forest と XGBoost の予測式

Random Forest は $T$ 本の木の予測を単純平均: $\hat y = \frac{1}{T}\sum_{t=1}^T f_t(x)$。 一方 XGBoost は加法モデル: $\hat y = \sum_{t=1}^T \eta f_t(x)$ で、 各 $f_t$ は前の残差にフィットし学習率 $\eta$ で減衰させる。 つまり Bagging は「投票」、 Boosting は「修正」が本質。

🎯 このコードでやること

SSDSE-B-2026 の都道府県データで人口(A1101)を 出生数(A4101)・転入者数(A5101)・死亡数(A4200)等の特徴量から予測し、 RandomForest / XGBoost / LightGBM の R^2 を比較する。

📥 入力データ: SSDSE-B-2026.csv の都道府県 47 行

都道府県 A1101 A4101 A5101 0 北海道 5092000 24430 47388 1 青森県 1184000 5696 15226 2 岩手県 1163000 5432 14903 3 宮城県 2264000 12328 42940 4 秋田県 914000 3611 10002
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score, KFold
import xgboost as xgb
import lightgbm as lgb

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).query('`SSDSE-B-2026` == 2023')  # 2023 年に絞る(B は複数年度を含むため)
# 目的変数: 総人口 (A1101)、 説明変数: 出生数・転入者数・死亡数
features = ['A4101', 'A5101', 'A4200']
data = df[features + ['A1101']].dropna()
X, y = data[features], data['A1101']

cv = KFold(n_splits=5, shuffle=True, random_state=42)
models = {
    'RF':   RandomForestRegressor(n_estimators=500, random_state=42),
    'XGB':  xgb.XGBRegressor(n_estimators=500, learning_rate=0.05),
    # 訓練は各 fold 37 件ほど。葉の最小件数が既定 20 のままだと 1 回も分岐できない
    'LGBM': lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05, verbose=-1, n_jobs=1,
                              min_child_samples=3),
}
for name, model in models.items():
    r2 = cross_val_score(model, X, y, cv=cv, scoring='r2').mean()
    print(f'{name}: R^2 = {r2:.4f}')
rf = models['RF'].fit(X, y)
print(pd.Series(rf.feature_importances_, index=features).round(3))

📤 実行例:

RF: R^2 = 0.9473 XGB: R^2 = 0.9330 LGBM: R^2 = 0.8565 A4101 0.408 A5101 0.211 A4200 0.381 dtype: float64

💬 結果の読み方: 2023 年 47 県のみ(年度リークなし)で評価すると RF が R²≈0.95 と最も高く、 XGBoost 0.93、 LightGBM 0.86 と続く。 LightGBM は葉の最小件数 min_child_samples を既定の 20 のまま使うと、 各 fold の訓練 37 件ほどでは 1 回も分岐できずに全県を平均値で予測し、 R² が −0.11 と平均予測以下に落ちる(小データで LGBM が動かない典型)。 feature_importance では出生数 0.408・死亡数 0.381 が人口規模の主説明変数(いずれも人口と強く相関)で、 転入者数は 0.211。 モデル選択は「大データの定番がそのまま小データで最良とは限らない」ことを示す。

⚠️ 落とし穴

📊 feature importance 3 方式の使い分け

「どの特徴が効いたか」を測る方法は 1 つではない。 3 方式の長短を整理する。

方式計算原理長所短所
Gini Importance分岐での不純度減少の合計学習時に計算済 (速い)高カーディナリティ変数を過大評価
Permutation Importance特徴をシャッフルしてスコア低下を測定モデル非依存 / バイアス少相関する特徴を分散して見せる
SHAP 値ゲーム理論の Shapley 値各サンプルごとの寄与 (局所説明)計算コスト高

SSDSE-B-2026 の人口予測タスクでは、 出生数 (A4101) は人口規模と機械的に相関するためどの方式でも上位だが、 SHAP 値で見ると「東京は出生数の絶対値が大きいから人口予測が高く出る」「沖縄は出生率が高いが絶対人口は小さいので影響は中程度」など、 サンプル固有の寄与が明らかになる。

「決定木・アンサンブル学習」を扱う際の手法選択は、 状況に応じて以下のフローで判断すると迷いが減る。

  1. Step 1: 目的は記述か予測か?
    • 記述 (現状把握・要約) → 集計・可視化・要約統計量で全体像を掴む
    • 予測 (未知データへの推定) → モデル構築・検証フェーズへ移行
  2. Step 2: データの種類・規模は?
    • 数値データ・小〜中規模 → 「決定木・アンサンブル学習」やその拡張手法を直接適用
    • カテゴリデータ → カテゴリ専用の手法 (関連用語) と組み合わせ
    • 大規模・高次元 → 計算効率を考慮した派生手法 (関連用語) を選択
  3. Step 3: 結果の解釈・共有は?
    • 専門家向け → 数値指標・統計検定で精緻に評価
    • 非専門家向け → 可視化・自然言語での要約を重視

このフローに沿って判断することで、 「決定木・アンサンブル学習」を中核とした適切な手法選択ができる。

🧭 解説を深める: 木アンサンブルは「内挿装置」である

姉妹ページ(ランダムフォレスト・XGBoost)が「精度をどう上げるか」を扱うのに対し、 この節は逆側、 木アンサンブルが構造上できないことに焦点を当てる。 題材はすべて SSDSE-B-2026(47 都道府県・2023 年)の実測値で、 合成データを使う箇所は「架空」と明記する。

💡 直感: 予測は必ず「訓練で見た値」の内側に落ちる

決定木の予測とは、 サンプルが落ちた 葉に入っている訓練目的変数の平均を返すこと。 アンサンブルはその平均の重み付き和(バギングは単純平均、 ブースティングは残差の逐次和)なので、 出力は原理的に 訓練時の目的変数の値域の内側に収まる。 平均も多数決も「内側に丸める」操作だからだ。 線形回帰が直線を延ばして訓練範囲の外まで予測を出せるのと、 ここが決定的に違う。

🕳 落とし穴(重要): 外挿の天井 と 重要度のバイアス

① 外挿の天井(実測)。 総人口(A1101)を出生数(A4101)と年平均気温(B4101)から予測するモデルを組み、 東京都だけを訓練から外して予測させると次のようになる(scikit-learn 既定設定、 実測値)。

項目総人口(人)
東京都の実測値14,086,000
訓練データの最大値(神奈川県)9,229,000
RandomForest の東京都予測8,206,667
GradientBoosting の東京都予測8,117,916

両手法とも 訓練最大値(神奈川 922.9 万人)を超えられず、 実測の 約 58%(RF は 8,206,667÷14,086,000)で頭打ちになった。 東京は「訓練で見たどの県より人口が多い」外挿点なので、 木の葉に入っている平均をいくら合議しても届かない。 これは実装のバグではなく アルゴリズムの性質だ。 時系列で「2030 年の人口・高齢化率」を木モデルだけで外挿すると、 過去最大値で必ず飽和する。 将来の外挿が要るタスクでは ARIMA / Prophet など外挿できるモデルと併用するのが定石(本ページ本文の「木モデルが苦手なケース」とも整合)。

② 特徴量重要度のバイアス(実測 + 架空列)。 上の総人口予測モデルに、 意味のない一様乱数列(架空・47 個すべて異なる値の高カーディナリティ列)を 1 本混ぜて RandomForest を学習し、 2 種類の重要度を比べた(実測)。

特徴量impurity 重要度permutation 重要度
A4101 出生数0.9830.998
B4101 年平均気温0.0100.001
架空の乱数列(無意味)0.0080.001

完全に無意味な乱数列が、 feature_importances_(impurity ベース)では 0.008 と気温(0.010)に迫る値を得ている。 これは「連続値でユニーク値が多い列ほど、 in-sample でノイズに分岐点を合わせられ、 不純度減少を稼いでしまう」という 高カーディナリティ・バイアスの実例だ。 permutation 重要度で測り直すと乱数列は 0.001 とほぼゼロに落ちる。 n=47 の小標本ほどこの偽の信号は出やすい。 既定の重要度を鵜呑みにせず、 permutation importance か SHAP で裏取りすること。

🚀 発展: 天井を破る/偽の重要度を避ける実務手

🎮 触って確かめる: 外挿の天井(架空データ)

下は 架空の 1 次元データ(訓練点は網掛け範囲の内側だけに存在)。 スライダー(またはグラフを左右にドラッグ/スワイプ)で予測したい x を動かすと、 木アンサンブル(緑)は訓練範囲の外へ出た瞬間 水平に飽和し、 線形回帰(青)だけが直線を延ばして外挿するのが見える。