論文記事から各用語のリンクをクリックすると、 該当箇所が開きます:
🍰 まずはやさしく
YesかNoで分ける仕組みです。
正しい答えを導き出すために使います。
買い物で商品を選ぶときのような判断です。
この章では決定木と学習の方法を学びます。
🍰 まずはやさしく
弱い力を合わせて強くする方法です。
予測のミスを減らすために使います。
部活でみんなの意見をまとめる感覚です。
ここでは複数のモデルを組む方法を読みます。
本ページは、 決定木を出発点として、 バギング・ランダムフォレスト・ブースティング(AdaBoost / 勾配ブースティング / XGBoost / LightGBM / CatBoost)・スタッキングまでを一気通貫で解説する統合ページです。
これらは「弱い学習器を多数組み合わせて強い学習器を作る」というアンサンブル学習の発想に基づきます。 SSDSE-B のような中規模テーブルデータでは、 深層学習よりこれらが第一選択となることが多いです。
本ページの「ツリーアンサンブル」は 機械学習における集成学習 (ensemble learning) の決定木版。 単一木が抱える「過学習しやすい・分散が大きい」弱点を、 複数木の組み合わせで克服する手法群を指します。
🍰 まずはやさしく
審査員による多数決のようなものです。
一人だけの判断による偏りをなくします。
クラスの出し物をみんなで決める例です。
ここでは合議制の仕組みを直感的に学びます。
単一の決定木は「1 人の審査員」に過ぎません。 ツリーアンサンブルは「100 人の審査員」を集めて多数決 (分類) や平均 (回帰) を取る合議制。 個々の審査員は偏っていても、 多数決により偏りが相殺されて全体精度が向上します。
| 手法 | 学習方法 | 直感的説明 | 代表ライブラリ |
|---|---|---|---|
| Random Forest | Bagging (並列) | 独立な審査員 100 人が同時に意見、 多数決 | sklearn |
| GBDT | Boosting (逐次) | 前審査員の誤りを次が修正、 リレー方式 | sklearn, lightgbm |
| XGBoost | Boosting + 正則化 | GBDT に L1/L2 + 並列化、 Kaggle 標準 | xgboost |
| LightGBM | Boosting + leaf-wise | GBDT に leaf-wise tree、 大規模データで高速 | lightgbm |
| CatBoost | Boosting + Ordered TS | GBDT にカテゴリ変数のリーク防止 | catboost |
| Isolation Forest | Bagging (異常検知) | 「変な例ほど早く孤立する」性質を利用 | sklearn |
| 条件 | 推奨手法 | 理由 |
|---|---|---|
| 表形式データ (テーブル) | GBDT (XGBoost/LightGBM) | 2024 年の比較でも依然 SOTA |
| 画像 | CNN/Vision Transformer | 空間構造の学習はツリーでは困難 |
| テキスト | Transformer (BERT/GPT) | 系列依存・文脈学習が必要 |
| 特徴量数 100 以下 | RF or GBDT | DL は学習データ不足になりやすい |
| 特徴量数 1000 以上 | DL or 線形 + L1 | ツリーは高次元で速度問題 |
| 解釈性が必須 | RF + SHAP | decision path を辿れる |
| サンプル数 100 以下 | RF or LR | DL は過学習しやすい |
| 年 | 出来事 | 提案者 |
|---|---|---|
| 1984 | CART (決定木) の提案 | Breiman et al. |
| 1996 | Bagging の提案 | Breiman |
| 1996 | AdaBoost | Freund & Schapire |
| 2001 | Random Forest 命名 | Breiman |
| 2001 | Gradient Boosting Machine | Friedman |
| 2014 | XGBoost 公開 | Chen & Guestrin |
| 2017 | LightGBM 公開 | Microsoft |
| 2017 | CatBoost 公開 | Yandex |
| 2017 | SHAP (TreeExplainer) | Lundberg & Lee |
| 2022 | XGBoost 2.0 (multi-output, GPU) | DMLC |
| 2024 | 表形式 DL モデル群との比較で依然 SOTA | Grinsztajn et al. |
2024 年現在、 表形式データに関しては GBDT が依然として SOTA を維持。 DL モデル (TabNet、 SAINT、 FT-Transformer) も近づいているが、 小〜中規模では GBDT が優位。
決定木のアンサンブル(バギング / ランダムフォレスト / ブースティング)の中核を 3 枚の図で再整理する。 並列の多数決と、 逐次の誤差補正、 その境界を視覚化する。
単一の決定木は学習データに過剰適合しやすく、 境界がギザギザに崩れる。 多くの木をブートストラップで作って平均すれば、 境界は 滑らかになり汎化性能が大きく改善する。
💬 読み方:左の赤線(単一木)は階段状でデータの境目をなぞるため、 ノイズに敏感。 右の緑線(RF)は多数の木の 多数決 なので、 個々の木の不安定さがキャンセルされ、 滑らかで頑健な境界になる。
2 大アンサンブル流派の根本的な違い。 バギングは ブートストラップ標本で並列学習し平均、 ブースティングは 前の木の残差を次の木が補正する逐次学習。 計算の流れ自体が異なる。
💬 読み方:左(青)の バギングは元データから複数の bootstrap 標本 → 並列に深い木を学習 → 平均。 右(オレンジ)のブースティングは 浅い木 を逐次的に積み、 各木は前の木の残差を補正する。 並列化のしやすさはバギング、 精度の極限はブースティング。
アンサンブルの効用は 「分散低減(バギング)」 と 「バイアス低減(ブースティング)」の 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 スコアです。 各木はブートストラップサンプル(重複あり抽出)で学習しますが、 1 つの木から見ると約 37% のサンプルが「学習に使われなかった」サンプルになります。 これらを使って自動的に検証スコアを計算できるので、 別途 cross-validation を組まなくても汎化性能の推定値が手に入ります。 scikit-learn では RandomForestRegressor(oob_score=True) と指定するだけで、 学習後に rf.oob_score_ から R² を取得できます。 SSDSE-B-2026 のような小規模データでは「CV を組むと 1 fold あたり 9〜10 件しか残らない」問題があるので、 OOB スコアは特に役立ちます。
木モデルの特徴量重要度には主に 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 つのハイパラを総合的に調整します。 (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 と組み合わせるのが現実的です。
6 問中 5 問以上正解で「木アンサンブルの使い分けと運用を理解した」と自己評価できる。 5 問未満なら「使い分け」「特徴量重要度」「過学習を防ぐレバー」の節を再読する。
勾配ブースティング系の 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 (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 (QRF) を使うと予測の信頼区間を出せます。 QRF は学習時の各葉に観測値の分布を保存しておき、 推論時に「予測値 ± 信頼区間」を出力します。 SSDSE-B-2026 で「秋田県の 2030 年高齢化率は 39.2% ± [37.5%, 41.0%]」のような区間予測ができ、 政策判断に直結する不確実性情報を提供できます。 Python では quantile-forest パッケージや scikit-learn の GradientBoostingRegressor(loss='quantile', alpha=0.9) で実装できます。
木アンサンブルでは、 単木数、深さ、学習率、サブサンプリング、特徴量重要度の種類を明示します。 重要度は予測への寄与を示す手がかりであり、 因果効果と混同しないよう注意します。
同じ「浅い回帰木」を材料にしても、 組み合わせ方が違うとアンサンブルの性格は一変します。 下のデモは、 非線形な 1 次元回帰データ(真の関数 $f(x)=\sin 2\pi x$ +ノイズ、 46 点)に対して、 (A) バギング=ブートストラップ標本ごとに独立に木を学習して平均、 (B) ブースティング=前段までの残差に木を逐次フィットして加算、 を同一条件で切り替えて観察できます。 木の計算は近似ではなく、 CART と同じ「二乗誤差を最小化する厳密な貪欲分割」で行っています。
💡 グラフ上を左右にドラッグ(スマホはスワイプ)しても木の本数 M を変えられます。
| 観点 | バギング(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 必須) |
| 並列化 | 容易(木ごとに独立計算) | 木単位では不可(分割探索内部のみ並列化) |
max_depth を絞りすぎるとバイアスが支配的になります。 逆にブースティングに深すぎる木を使うと数本で過学習します。※ デモのデータは動作説明用に生成した合成データ(乱数シード固定)であり、 SSDSE-B-2026 の実測値ではありません。 実データでの数値例は本ページ「🧮 SSDSE-B-2026 で実値計算」の章を参照してください。
🍰 まずはやさしく
計算で答えを出すためのルールです。
予測の精度を正しく測るために使います。
スマホのアプリが計算する仕組みに似ています。
ここでは中核となる数式について読みます。
🔬 数式を言葉で読み解く:M 個のブートストラップ標本でそれぞれ木 \\(\\hat{f}_m\\) を学習し、 出力を平均 (回帰) または多数決 (分類)。 分散が \\(\\sigma^2 / M\\) に下がる → 過学習が緩和される。
🔬 数式を言葉で読み解く:各ノードで全 p 特徴量から \\(m_{\\mathrm{try}}\\) 個だけランダム選択。 木同士の相関を下げて bagging の効果を最大化。 これが RF が「単純 bagging より優れる」核心理由。
🔬 数式を言葉で読み解く:第 t 段で前段の予測 \\(F_{t-1}\\) に学習率 \\(\\eta\\) (0.01〜0.1) 倍した新木 \\(h_t\\) を加算。 \\(h_t\\) は損失 L の負の勾配 (residual) を fit する。 学習率を小さくすると過学習しにくいが時間がかかる。
🔬 数式を言葉で読み解く:木の葉数 T に \\(\\gamma\\)、 葉の重み \\(w\\) に L2 ノルム \\(\\lambda\\) で正則化。 GBDT に「木の複雑さ」penalty を入れたものが XGBoost の本質。
🔬 数式を言葉で読み解く:特徴量 j が分割に使われたノードでの Gini 減少量を全木で平均。 「この特徴量が無いとどれだけ精度が落ちるか」の代理指標。 ただしカテゴリ数が多いとバイアスがかかる (Permutation Importance 推奨)。
バギングが「なぜ精度を上げるか」を分散減少の観点から数理的に説明します。
🔬 数式を言葉で読み解く: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)) |
📤 実行結果:
💬 結果の読み方:ρ=0 なら 99% 分散減少、 ρ=0.5 なら半分にしか減らない、 ρ=0.8 なら 20% しか減らない。 Random Forest が「ノードで特徴量サブサンプル」する設計思想がここに直結している。
🎯 このコードでやること: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}') |
📤 実行結果:
💬 結果の読み方:既定パラメータのままの 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 を読み込み、 総人口を 3 クラス (大: 500 万人超、 中: 100〜500 万、 小: 100 万未満) に分け、 出生率・気温・降水量などから RF で分類。 feature_importances_ で寄与度を確認。
📥 入力データ (SSDSE-B-2026 抜粋、 主要列):
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)) |
📤 実行結果:
💬 結果の読み方:2023 年の 47 県のみ(年度リークなし)で 5-fold CV 約 94% 精度。 最重要特徴量は 着工建築物数 (C3301)・死亡数 (A4200)・出生数 (A4101) — いずれも人口規模と強く連動する量。 気候要因(気温・降水)の寄与は小さい。 RF はこのように「どの特徴が効くか」を可視化できる強み。
🎯 このコードでやること:同じ問題を 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') |
📤 実行結果:
💬 結果の読み方:2023 年 47 県では RF と GBDT はほぼ同水準(RF 0.938 / GBM 0.916)で、 木の本数が少ない小規模データでは両者の差は小さい。 学習時間は GBDT のほうが長い。 大規模データ (10 万行以上) では LightGBM の leaf-wise growth が顕著に高速。
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)) |
📤 実行結果:
💬 結果の読み方:2023 年 47 県のみに絞ると、 人口規模クラス(大/中/小)は着工建築物数・出生数・死亡数からほぼ完全に線形分離できてしまい、 どの深さでも train=test=100%、 gap=0 になる。 つまりこのターゲットは易しすぎて過学習が観測されない。
予測結果を「どの特徴がどれだけ寄与したか」を SHAP 値で説明します。 個別予測の解釈に強い。
🎯 このコードでやること:GBDT モデルの予測に対し 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)) |
📤 実行結果:
💬 結果の読み方:2023 年 47 県で RandomForest に shap.TreeExplainer を適用した実測値。 出生数・死亡数・着工建築物数の 3 つが人口規模クラスの判別に効き、 気候変数の寄与は 1 桁小さい。 Gini Importance と概ね一致するが、 SHAP は 個別予測ごとの寄与も分解できる点が強み。 なお sklearn の多クラス GradientBoostingClassifier は TreeExplainer 非対応のため、 ここでは RF を用いている。 ブラックボックス批判への強力な回答。
合成 5 木の予測から多数決を計算する。
| サンプル | t1 | t2 | t3 | t4 | t5 | 多数決 |
|---|---|---|---|---|---|---|
| s1 | 1 | 1 | 0 | 1 | 1 | 1 |
| s2 | 0 | 1 | 0 | 0 | 1 | 0 |
| s3 | 1 | 0 | 1 | 1 | 0 | 1 |
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}") |
💬 手計算 (Step 2) と Python 出力が完全一致。
同じ「高齢化率の高低(中央値で二値化)」の分類を 4 つのライブラリで実装する例。 API の違いと共通点が分かります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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)), ]: sc = cross_val_score(model, X, y, cv=5, scoring='accuracy') print(f'{name}: {sc.mean():.3f} ± {sc.std():.3f}') |
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) |
1 2 3 4 5 6 7 8 9 10 11 | 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) model = lgb.LGBMClassifier(n_estimators=1000, learning_rate=0.05, verbose=-1) 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() |
「Random Forest を使った結果、 accuracy 0.85 でした。」
「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 値による個別解釈でも同じ傾向を確認した。」
| 手法 | 主要パッケージ・クラス | 主要引数 |
|---|---|---|
| 決定木 | sklearn.tree.DecisionTree{Classifier,Regressor} | criterion, max_depth, min_samples_leaf, ccp_alpha |
| Random Forest | sklearn.ensemble.RandomForest{Classifier,Regressor} | n_estimators, max_features, oob_score, class_weight |
| Extra Trees | sklearn.ensemble.ExtraTrees{Classifier,Regressor} | RFと同じ + 分岐閾値ランダム |
| AdaBoost | sklearn.ensemble.AdaBoostClassifier | n_estimators, learning_rate, algorithm |
| GBM | sklearn.ensemble.GradientBoosting{Classifier,Regressor} | n_estimators, learning_rate, max_depth, subsample |
| HistGBM | sklearn.ensemble.HistGradientBoosting{Classifier,Regressor} | LightGBM相当の高速実装 |
| XGBoost | xgboost.XGB{Classifier,Regressor} | n_estimators, eta, max_depth, lambda, alpha, gamma |
| LightGBM | lightgbm.LGBM{Classifier,Regressor} | n_estimators, learning_rate, num_leaves, feature_fraction |
| CatBoost | catboost.CatBoost{Classifier,Regressor} | iterations, learning_rate, depth, cat_features |
| Stacking | sklearn.ensemble.StackingClassifier | estimators, final_estimator, cv, passthrough |
| SHAP | shap.TreeExplainer | ツリー系専用の高速 SHAP |
🎯 このコードでやること: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)) |
📤 実行結果:
💬 結果の読み方:2023 年 47 県のみで正しく評価すると CV R² = -1.128(負!)── 平均値予測にすら負けており、 このモデルは汎化していない。 特徴量 7 個・サンプル 47 件では GBDT(n_estimators=300)が容易に過学習し、 fold 外の県をまったく当てられない。 訓練データ上の feature_importances_(C3301 が最大)は「見かけの寄与」に過ぎない。
🎯 このコードでやること: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}') |
📤 実行結果:
💬 結果の読み方:2023 年 47 県では単独 RF が 0.938 と最も高く、 Stack (0.916) は RF を上回らなかった。 線形モデル (LR 0.727) が弱いターゲットでは、 スタッキングが必ずしも単独ベストを超えるとは限らない ── 特にサンプルが 47 件と少ないと meta 学習の恩恵が出にくい。 多様性のあるモデルを混ぜる手法自体は Kaggle 上位解の常套手段だが、 「必ず勝つ」わけではない点に注意。
🎯 このコードでやること: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)) |
📤 実行結果:
💬 結果の読み方:人口・出生数・消費支出が突出する東京、 気温・降水量が全国から大きく乖離する沖縄、 大きな人口と低い気温を併せ持つ北海道が「異常」と判定される (2023 年値、 実測)。 これは「データ的に異常」というだけで「悪い」わけではない (= 特徴量空間での外れ値)。 異常検知は「警告フラグ」であり、 「アクション」とは独立に解釈する。
| 落とし穴 | 対処 |
|---|---|
| 1本の木を信用しすぎる | アンサンブルで分散低減。 訓練 data の僅かな違いで木は大きく変わる。 |
| 特徴量重要度を絶対視 | MDI バイアス。 Permutation Importance / SHAP を併用。 |
| 外挿(範囲外予測) | ツリーは外挿できない。 訓練データの範囲外は予測しないかドメイン知識で補正。 |
| ブースティングの早期停止忘れ | early_stopping_rounds を必ず設定。 過学習防止と高速化に必須。 |
| 時系列データに通常CV | TimeSeriesSplit を使用。 リークを避ける。 |
| クラス不均衡 | scale_pos_weight / class_weight / is_unbalance を設定。 |
| 前処理過剰 | ツリー系はスケーリング不要。 単調変換でも結果は不変。 |
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」は予測への寄与であり、 因果効果ではない。
feature_importances_、 SHAP 値は「モデルがその特徴量をどれくらい予測に使ったか」を示すだけです。Tree ensemble (Random Forest / Gradient Boosting / XGBoost / LightGBM / CatBoost) を中心に、 単独決定木 (CART) を前提に、 バギング / ブースティング / スタッキングへの派生、 特徴量重要度・SHAP への接続を 6 方向に整理。
Tree ensemble の中心は Random Forest・Gradient Boosting・XGBoost・LightGBM・CatBoost の 5 系統。 前提に 単独の決定木 (CART)、 並列に バギング / ブースティング、 発展に スタッキング / ヘテロアンサンブル がある。
SSDSE-B-2026 の「都道府県の人口・出生・着工建築物 → 消費支出(L3221)」を予測する場合、 1 本の決定木は MAE が大きいが、 RF (n_estimators=500) や LightGBM で 47 県 CV すると MAE が 1/3 になる。 これがアンサンブルの「分散低減」効果の実例。
Tree ensemble は表形式データの「定番モデル」で、 前処理から評価まで明確なパイプラインがある。
SSDSE-B-2026 (47 県 × 50 指標) のような中小規模データでは LightGBM + 5-fold CV + Early Stopping が標準。 NN より高速で精度も高い。
バギング + 各分岐で特徴量もランダムサンプリング。 木同士の相関が下がり、 平均化効果がさらに高まる。
n_estimators:木の本数。 多いほど安定(100〜1000)max_depth:木の深さ上限(過学習防止)max_features:各分岐で使う特徴量数。 分類は $\sqrt{p}$、 回帰は $p/3$ が標準min_samples_leaf:葉ノード最小サンプル数(過学習防止)class_weight='balanced':不均衡クラス対応分岐に使った特徴量が平均でどれだけ不純度を下げたかで重要度を算出(mean decrease in impurity)。
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 バイアス)。 より信頼性の高い順列重要度(Permutation Importance)や SHAP を併用するのが実務標準。
弱学習器を直列に並べ、 前の学習器の誤りを次が補正するように学習を進める。
誤分類したサンプルの重みを大きくし、 次の弱学習器がそれを優先的に学ぶ。
$$\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$ が小さい学習器ほど大きな影響を持つ。
損失関数 $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)) |
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)) |
| 項目 | XGBoost | LightGBM | CatBoost |
|---|---|---|---|
| 速度 | 速い | 最速 | やや遅 |
| カテゴリ | 事前エンコード必要 | 対応 | 最強対応 |
| 既定値の精度 | 中 | 中 | 高 |
| 過学習耐性 | 中 | 過学習しやすい | 強い |
| エコシステム | 豊富 | 豊富 | 中 |
複数の異質モデル(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)) |
ツリー系モデルの予測寄与をサンプル単位で分解する。 「なぜこの個体は『高い』と予測されたのか」が分かる。
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()) |
| 手法 | 精度 | 速度 | 解釈性 | 過学習 | 推奨用途 |
|---|---|---|---|---|---|
| 決定木 | 低〜中 | 速 | 高 | しやすい | 説明用途・ルール抽出 |
| Random Forest | 中〜高 | 中 | 中 | 強い | 汎用ベースライン |
| XGBoost / LightGBM | 高 | 中〜高 | 中 | CV必須 | 本番モデル(テーブル) |
| CatBoost | 高 | 中 | 中 | 既定値で強い | カテゴリ多めデータ |
| Stacking | 最高 | 遅 | 低 | CV慎重に | 最後の伸ばし |
木のアンサンブルは「弱学習器(浅い決定木)を多数組み合わせて強学習器を作る」枠組み。 組み合わせ方の違いで Bagging / Boosting / Stacking の 3 系統に分かれる。 SSDSE-B-2026 の都道府県人口予測タスクで比較する。
| 系統 | 学習方式 | 分散低減 | バイアス低減 | 代表アルゴリズム |
|---|---|---|---|---|
| Bagging | 並列・独立に学習し平均 | ◎ | △ | Random Forest, Extra Trees |
| Boosting | 直列・残差を逐次フィット | ○ | ◎ | XGBoost, LightGBM, CatBoost |
| Stacking | メタ学習器で組み合わせ | ○ | ○ | sklearn StackingRegressor |
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 行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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), 'LGBM': lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05, verbose=-1, n_jobs=1), } 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)) |
📤 実行例:
💬 結果の読み方: 2023 年 47 県のみ(年度リークなし)で評価すると RF が R²≈0.95 と最も安定し、 XGBoost も 0.93。 一方 LightGBM は R²=-0.11 と破綻 ── 既定の n_estimators=500・葉ベース成長は 47 件では過剰で、 min_child_samples 等を絞らないと機能しない(小データで LGBM が不安定になる典型)。 feature_importance では出生数・死亡数が人口規模の主説明変数(いずれも人口と強く相関)。 モデル選択は「大データの定番がそのまま小データで最良とは限らない」ことを示す。
「どの特徴が効いたか」を測る方法は 1 つではない。 3 方式の長短を整理する。
| 方式 | 計算原理 | 長所 | 短所 |
|---|---|---|---|
| Gini Importance | 分岐での不純度減少の合計 | 学習時に計算済 (速い) | 高カーディナリティ変数を過大評価 |
| Permutation Importance | 特徴をシャッフルしてスコア低下を測定 | モデル非依存 / バイアス少 | 相関する特徴を分散して見せる |
| SHAP 値 | ゲーム理論の Shapley 値 | 各サンプルごとの寄与 (局所説明) | 計算コスト高 |
SSDSE-B-2026 の人口予測タスクでは、 出生数 (A4101) は人口規模と機械的に相関するためどの方式でも上位だが、 SHAP 値で見ると「東京は出生数の絶対値が大きいから人口予測が高く出る」「沖縄は出生率が高いが絶対人口は小さいので影響は中程度」など、 サンプル固有の寄与が明らかになる。
「決定木・アンサンブル学習」を扱う際の手法選択は、 状況に応じて以下のフローで判断すると迷いが減る。
このフローに沿って判断することで、 「決定木・アンサンブル学習」を中核とした適切な手法選択ができる。
姉妹ページ(ランダムフォレスト・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.983 | 0.998 |
| B4101 年平均気温 | 0.010 | 0.001 |
| 架空の乱数列(無意味) | 0.008 | 0.001 |
完全に無意味な乱数列が、 feature_importances_(impurity ベース)では 0.008 と気温(0.010)に迫る値を得ている。 これは「連続値でユニーク値が多い列ほど、 in-sample でノイズに分岐点を合わせられ、 不純度減少を稼いでしまう」という 高カーディナリティ・バイアスの実例だ。 permutation 重要度で測り直すと乱数列は 0.001 とほぼゼロに落ちる。 n=47 の小標本ほどこの偽の信号は出やすい。 既定の重要度を鵜呑みにせず、 permutation importance か SHAP で裏取りすること。
linear_tree=True は葉に定数でなく線形式を置くため、 葉の内側で傾きを持て、 純粋な階段関数よりは端の飽和が緩む。下は 架空の 1 次元データ(訓練点は網掛け範囲の内側だけに存在)。 スライダー(またはグラフを左右にドラッグ/スワイプ)で予測したい x を動かすと、 木アンサンブル(緑)は訓練範囲の外へ出た瞬間 水平に飽和し、 線形回帰(青)だけが直線を延ばして外挿するのが見える。