- AI ⚙️
ランダムフォレストとは|バギングを自作し特徴量重要度の偏りを実測
結論
ランダムフォレストは、訓練データを復元抽出して作った多数の決定木に、分割のたびに一部の特徴量だけを候補として選ばせ、木の予測(確率)を平均して答えを出す機械学習の手法です。手元の乳がんデータ(5分割交差検証)では、1本の決定木の正解率 0.926 が 100本の森で 0.965 に上がりました。numpy でバギングを自作すると、scikit-learn と予測確率・OOB スコア・特徴量重要度まで誤差0で一致します。
この記事でわかること
- ランダムフォレストの仕組み(ブートストラップ・特徴量のランダム選択・平均)と、なぜ1本の木より強いのかを、木どうしの相関の実測で
- Python(scikit-learn 1.9.1)での使い方、木の本数と精度・時間・サイズ、OOB スコア、ハイパーパラメータの決め方とエラー原文
- 特徴量重要度(不純度ベースと permutation)の違いと、ただの乱数が重要度1位になる偏りの実例、XGBoost など勾配ブースティングとの判断表
この記事で実際に動かしたもの:scikit-learn の RandomForestClassifier・RandomForestRegressor・ExtraTreesClassifier・HistGradientBoostingClassifier と、numpy で自作したバギング(ブートストラップ・OOB・不純度ベースの重要度・permutation 重要度)。データは scikit-learn 同梱の load_breast_cancer・load_wine・load_digits・load_diabetes と、make_classification・自作の架空データだけです。XGBoost 本体はこのマシンに入っていないため動かしていません。
flowchart TD
A[訓練データ n 件] --> B[復元抽出で n 件<br>木ごとに別の組]
B --> C[決定木を B 本育てる<br>分割ごとに特徴量を一部だけ]
C --> D[全部の木の確率を平均]
D --> E[最大のクラスが予測]
B -.->|残りの約37%| F[OOB で評価]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| ランダムフォレストとは | 復元抽出したデータと、分割ごとに選ぶ一部の特徴量で多数の決定木を作り、平均(分類は確率の平均)で予測するアンサンブル学習 | ランダムフォレストとは |
| ランダムフォレスト python | RandomForestClassifier(n_estimators=200, oob_score=True) を fit するだけ。標準化は要らない | 最初の5分 |
| 決定木との違い | 1本の木は訓練データに合わせすぎて揺れる。多数の木を平均すると揺れ(分散)が減る | アンサンブル学習とバギング |
| バギングとの違い | バギングは全部の特徴量で木を作る。ランダムフォレストは分割ごとに一部だけを候補にし、木どうしを似せない | 特徴量のランダム選択 |
| 木の本数(n_estimators) | 多いほど安定し、ある本数で頭打ち。手元では100〜200本でほぼ横ばい、時間とサイズは本数に比例 | 木の本数と精度・時間 |
| OOB スコアとは | 各木の学習に使われなかった約37%のデータで測る精度。テストデータを分けずに汎化性能を見積もれる | OOB スコア |
| 特徴量重要度 | 既定の feature_importances_ は不純度ベース(MDI)。値の種類が多い特徴量を高く見せる偏りがあり、確かめには permutation を使う | 特徴量重要度の偏りを実測 |
| XGBoost との違い | 森は木を並列に独立に作って平均、ブースティングは前の木の誤りを次の木が直す。調整の手間と精度の伸びが違う | 勾配ブースティングとの判断表 |
確認環境:Ubuntu 24.04.5 LTS(CPU 2コア・メモリ8GB、ほかの作業と同時に動いている共有のマシン)で、Python 3.13.18、scikit-learn 1.9.1、numpy 2.5.3(OpenBLAS 0.3.34、OPENBLAS_NUM_THREADS=1)、scipy 1.18.1 を使い、すべて n_jobs=1 で実行しました。データは scikit-learn 同梱のデータセットと乱数で作った架空のデータで、ネットからの取得はしていません。時間は共有のマシンでの値のため10〜20%ぶれます。既定値・引数・警告の文言は手元の scikit-learn 1.9.1 で出したもので、公式ドキュメント(scikit-learn 1.9.1 の User Guide「Ensembles」と RandomForestClassifier の API リファレンス)と Breiman(2001)の原論文で確かめました。XGBoost は動かしておらず、比較には scikit-learn の HistGradientBoostingClassifier を使いました。図は mermaid で描画を確かめています(確認日 2026年11月5日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「ランダムフォレストとは|バギングを自作し特徴量重要度の偏りを実測」
ランダムフォレストとは(3つの仕組みと名前の意味)
この節では、ランダムフォレストが何をしているのかを、名前の意味と3つの仕組みで分かります。
ランダムフォレスト(random forest)は、Leo Breiman が2001年の論文「Random Forests」(Machine Learning 誌 45巻)でまとめた手法です。決定木をたくさん作り(森=forest)、作るときにわざと偶然(random)を入れて木どうしを似せないようにし、最後に平均をとります。分類にも回帰にも使え、表形式のデータでは「まず試す手法」の代表です。
言葉だけを見ると意味が取りにくいので、ふだんの意味と機械学習での意味を並べます。
| 言葉 | ふだんの意味 | ランダムフォレストでの意味 |
|---|---|---|
| フォレスト(森) | 木がたくさん集まった場所 | 決定木を100本、500本と集めたもの。scikit-learn では estimators_ に木が並ぶ |
| ランダム | でたらめ・無作為 | 木ごとに学習データを無作為に選び直すことと、分割ごとに特徴量の候補を無作為に選ぶことの2つ |
| バギング(bagging) | 袋(bag)に詰めること、と連想しやすい | bootstrap aggregating の略。復元抽出で作ったデータで別々に学習し、結果を集計する方法(Breiman 1996) |
| アンサンブル | 合奏・ひとそろい | 複数のモデルを組み合わせて1つの予測を出す学習の総称 |
1ブートストラップ(復元抽出)
n 件の訓練データから、同じものを何度選んでもよいとして n 件を選ぶ。木ごとに別の組になり、どの木にも選ばれない行が約37%できる。
2特徴量のランダム選択
木の各分割で、全部の特徴量ではなく一部(分類の既定は √特徴量数)だけを候補にして最良の分割を探す。強い特徴量ばかりが根に来るのを防ぐ。
3平均で集計する
回帰は木の予測の平均。分類は、原論文の多数決ではなく、scikit-learn は木の予測確率の平均をとる(公式 User Guide に明記)。
決定木そのもの(ジニ不純度・エントロピーでの分割の選び方・剪定)は、別記事「決定木とは」(公開準備中)で扱います。この記事は「木を集めるとなぜ強くなるのか」と「集めたときの使い方・落とし穴」に絞ります。分割の基準に使うエントロピーの計算は、関連記事「エントロピーとは|意味・計算・交差エントロピーをPythonで実演」で扱っています。
最初の5分:Pythonでランダムフォレストを動かす
この節では、scikit-learn でランダムフォレストを学習・評価する最小のコードと、その出力が分かります。
scikit-learn 同梱の乳がんデータ(569件・特徴量30個・良性/悪性の2クラス)を、訓練7割・テスト3割に分けて学習します。
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0, stratify=y)
rf = RandomForestClassifier(n_estimators=200, oob_score=True,
random_state=0, n_jobs=1)
rf.fit(X_train, y_train)
print("木の本数:", len(rf.estimators_))
print("テストの正解率:", round(rf.score(X_test, y_test), 4))
print("OOB スコア:", round(rf.oob_score_, 4))
print("1件目の確率:", rf.predict_proba(X_test[:1]).round(3))
木の本数: 200
テストの正解率: 0.9532
OOB スコア: 0.9698
1件目の確率: [[0.995 0.005]]
このコードには特徴量の標準化(StandardScaler)が入っていません。入れなくてよいのは、決定木が「ある特徴量がしきい値より大きいか」で分けるだけで、値の大きさの単位が変わっても分け方が変わらないからです。森も同じです。一方、random_state を決めないと、実行のたびに木が変わって数字が少しずつ動きます。比べる実験では必ず固定します。
手元の環境の版は、ターミナルで次の1行を実行すると確かめられます。この記事の数字は 1.9.1 のものです。
python3 -c "import sklearn, numpy; print(sklearn.__version__, numpy.__version__)"
1.9.1 2.5.3
アンサンブル学習とバギング(なぜ平均すると強くなるのか)
この節では、1本の決定木の弱点と、平均をとるとそれが減る理由を、式と実測で分かります。
深さを制限しない決定木は、訓練データをほぼ丸暗記します。手元の乳がんデータでは、1本の決定木(深さ6・葉16枚)の訓練の正解率は 1.000、テストは 0.906 でした。訓練データが少し変わるだけで木の形が大きく変わる、つまり予測の分散(揺れ)が大きいのが決定木の弱点です。
分散が大きいものは、平均すると揺れが減ります。同じ分布で分散 σ² の予測を B 個平均したとき、2つずつの相関が ρ なら、平均の分散は次の式になります(Hastie・Tibshirani・Friedman『The Elements of Statistical Learning』15章の式15.1)。
平均の分散 = ρσ² + (1 − ρ)σ² / B
B(木の本数)を増やすと第2項は0に近づきますが、第1項の ρσ² は残ります。つまり木どうしが似ている(ρ が大きい)と、何本集めても揺れが減りきらない。これを減らすために、ランダムフォレストは木の作り方に偶然を入れて ρ を下げます。バギングは1つ目の偶然(データ)だけ、ランダムフォレストはそれに2つ目の偶然(特徴量)を足したものです。
バギング(並列・独立)
木を互いに独立に作り、平均する
主に分散を減らす。木は深く育てるのが基本
ランダムフォレストはこちらの仲間
ブースティング(直列・逐次)
前の木の誤り(残差)を次の木が直す
主にバイアス(外れ方の偏り)を減らす。木は浅くするのが基本
XGBoost・LightGBM・HistGradientBoosting がこちら
アンサンブル学習にはこのほか、異なる種類のモデルの出力を別のモデルでまとめるスタッキングもあります。この記事では、バギングの代表であるランダムフォレストを掘り下げ、後の節でブースティングとの使い分けを判断表にします。
バギングをnumpyで自作してscikit-learnと一致させる
この節では、ランダムフォレストの学習の手順を自分のコードで書き、scikit-learn の結果と完全に一致することを確かめられます。
scikit-learn 1.9.1 のソース(sklearn/ensemble/_forest.py)を読むと、木ごとの処理は次の4段です。木の中の分割は sklearn の DecisionTreeClassifier に任せ、森の部分だけを自作します。
森の乱数から木ごとの種を引く
RandomState(random_state).randint(2³¹−1, size=B) で B 個の整数を作り、木 b の random_state にする。
その種で復元抽出する
RandomState(種).randint(0, n, n) で n 個の番号を選ぶ。同じ番号が何度も出る。
行を複製せず「選ばれた回数」を重みにする
np.bincount(番号, minlength=n) を sample_weight として木に渡す。0回の行は学習に使われない。
確率を平均して予測する
全部の木の predict_proba を足して B で割り、最大のクラスを答えにする。
import numpy as np
from sklearn.tree import DecisionTreeClassifier
class MyForest:
"""バギング+分割ごとの特徴量のランダム選択(木は sklearn の決定木)"""
def __init__(self, n_estimators=100, max_features="sqrt", random_state=0):
self.B, self.mf, self.rs = n_estimators, max_features, random_state
def fit(self, X, y):
n = len(X); self.classes_ = np.unique(y)
rng = np.random.RandomState(self.rs)
self.seeds = rng.randint(np.iinfo(np.int32).max, size=self.B)
self.trees, self.inbag = [], []
for seed in self.seeds:
idx = np.random.RandomState(seed).randint(0, n, n) # 復元抽出
w = np.bincount(idx, minlength=n) # 選ばれた回数
t = DecisionTreeClassifier(max_features=self.mf, random_state=seed)
t.fit(X, y, sample_weight=w)
self.trees.append(t); self.inbag.append(w > 0)
return self
def predict_proba(self, X):
return np.mean([t.predict_proba(X) for t in self.trees], axis=0)
def predict(self, X):
return self.classes_[self.predict_proba(X).argmax(1)]
def oob_score(self, X, y):
P = np.zeros((len(X), len(self.classes_)))
for t, ib in zip(self.trees, self.inbag):
P[~ib] += t.predict_proba(X[~ib]) # 学習に使わなかった行だけ
return (self.classes_[P.argmax(1)] == y).mean()
同じ random_state=42 で、自作と scikit-learn の両方を乳がんデータ(訓練398件・テスト171件)で学習して比べました。
time my 0.14s sk 0.16s
seeds equal: True
proba max abs diff: 0.0
pred equal: True
test acc my 0.9357 sk 0.9357
oob my 0.959799 sk 0.959799 covered 1.000 oob-rate-per-tree 0.3709 (1-1/e=0.3679)
hard vote acc 0.9357 differs from soft on 0 samples
single tree test 0.9064 train 1.0000 depth 6 leaves 16
木ごとの種・テストの予測確率(最大の差 0.0)・OOB スコアまで、すべて一致しました。ランダムフォレストの「森の部分」は、これだけの短いコードです。テストの1件目では、100本のうち99本が「クラス0」の確率1、1本が「クラス1」の確率1を返し、平均の [0.99, 0.01] がそのまま predict_proba になっていました。
多数決(各木の predict を数える)と確率の平均は、このデータでは171件すべて同じ答えでした。深く育てた木の葉は1クラスだけになりやすく、確率が0か1になるので、両者がほぼ同じになります。木を浅くしたり min_samples_leaf を大きくしたりして葉の確率が中間の値をとるようになると、2つの方式の答えが分かれる件が出てきます。配列の操作に慣れていない場合は、関連記事「NumPyの使い方 基本一覧|配列の作成・演算・形状変更」で扱っています。
特徴量のランダム選択はなぜ効くのか(max_featuresと木どうしの相関を実測)
この節では、max_features を変えると木どうしの相関と森の精度がどう変わるか、1本の木・バギング・ランダムフォレストの差を実測で分かります。
max_features と相関 ρ の関係
乳がんデータで木を200本作り、テストデータへの各木の予測確率どうしの相関の平均(ρ)を測りました。max_features=30 は全特徴量=ただのバギングです。
| max_features | 木1本の平均正解率 | 木どうしの相関 ρ | 森の正解率(テスト) | OOB スコア |
|---|---|---|---|---|
| 1 | 0.886 | 0.705 | 0.936 | 0.967 |
| 2 | 0.903 | 0.758 | 0.953 | 0.967 |
| 5(√30 の既定) | 0.911 | 0.794 | 0.953 | 0.970 |
| 10 | 0.915 | 0.816 | 0.947 | 0.965 |
| 20 | 0.914 | 0.825 | 0.930 | 0.960 |
| 30(=バギング) | 0.914 | 0.828 | 0.924 | 0.955 |
候補を減らすほど1本1本の木は弱くなります(0.914→0.886)が、木どうしの相関も下がり(0.828→0.705)、森としては既定の5前後がいちばん良くなりました。前の節の式の「ρσ² が残る」部分を、偶然で小さくしている様子がそのまま出ています。ただし1まで減らすと木が弱くなりすぎ、森の精度も落ちます。「木の強さ」と「木どうしの似ていなさ」の釣り合いを決めるのが max_features です。
回帰(load_diabetes、442件・特徴量10個)でも同じ傾向でした。木1本のテストの平均二乗誤差(MSE)は max_features=10 で 6,600、1 で 7,471 と悪くなる一方、残差どうしの相関は 0.529 から 0.438 に下がり、森の MSE は 3,492 から 3,272 に良くなりました。木1本の MSE の半分以下になっている点が、平均の効果です。
1本の木・バギング・ランダムフォレストを4つのデータで比べる
5分割交差検証の正解率の平均(± は標準偏差)です。「部分空間」は、分割ごとではなく木ごとに特徴量を √p 個だけ選ぶ方式(Ho 1998 のランダム部分空間法)を自作したもので、比較のために入れました。Extra Trees は、しきい値まで乱数で決める派生の手法です。
| データ(件数×特徴量) | 決定木1本 | バギング | ランダムフォレスト | Extra Trees |
|---|---|---|---|---|
| breast_cancer(569×30) | 0.926±0.022 | 0.960±0.023 | 0.965±0.015 | 0.968±0.009 |
| wine(178×13) | 0.927±0.051 | 0.966±0.021 | 0.972±0.018 | 0.989±0.014 |
| digits(1,797×64) | 0.859±0.020 | 0.952±0.018 | 0.973±0.007 | 0.979±0.004 |
| make_classification(1,000×40) | 0.836±0.036 | 0.915±0.026 | 0.913±0.033 | 0.919±0.026 |
どのデータでも、1本の木からバギングで大きく上がり(digits で 0.859→0.952)、特徴量のランダム選択でさらに上がるか同じ程度になりました。木ごとに特徴量を固定する部分空間法は、breast_cancer 0.954、digits 0.956、make_classification 0.879 と、分割ごとに選ぶランダムフォレストに届きませんでした。1本の木が使える特徴量が最初から限られると、効く特徴量を持たない木が弱くなるためと考えられます。
学習時間(5回分の合計)は digits でバギング 9.99秒、ランダムフォレスト 2.89秒、Extra Trees 2.42秒でした。分割のたびに調べる特徴量が64個から8個に減るので、ランダムフォレストはバギングより速くなります。
木の本数(n_estimators)と精度・時間・サイズ
この節では、木を何本にすればよいかを、精度・学習時間・予測時間・モデルのサイズの実測で判断できます。
digits(訓練1,257件・テスト540件)で本数だけを変えました。サイズは pickle で保存したときのバイト数です。
| 木の本数 | テスト正解率 | OOB スコア | 学習時間 | サイズ |
|---|---|---|---|---|
| 1 | 0.733 | 0.355(警告あり) | 0.01秒 | 144KiB |
| 10 | 0.948 | 0.850(警告あり) | 0.09秒 | 540KiB |
| 25 | 0.967 | 0.933 | 0.26秒 | 1,212KiB |
| 50 | 0.969 | 0.962 | 0.40秒 | 2,311KiB |
| 100(既定) | 0.970 | 0.974 | 0.88秒 | 4,528KiB |
| 200 | 0.978 | 0.973 | 1.70秒 | 9,035KiB |
| 500 | 0.976 | 0.975 | 3.97秒 | 22,362KiB |
本数を増やしても過学習はしません(精度が下がり続けることはない)が、ある本数で頭打ちになり、時間とサイズは比例して増えます。公式の User Guide も「多いほど良いが計算に時間がかかり、ある本数を超えると大きくは良くならない」と書いています。乳がんデータでは 100本で 0.953 に達し、200本・500本でも 0.953 のままでした。
予測の時間も本数に比例します。手元では200本の森で、1件の predict が中央値 20ms、540件まとめても 45ms でした。1件ずつ呼ぶと、木ごとの呼び出しの手間が件数に関係なくかかるため、まとめて予測するほうが1件あたりはずっと速くなります。API で1件ずつ応答する用途では、本数と木の深さを抑えるのが効きます。
本数を決めるときは、warm_start=True で木を足しながら OOB の誤差を見る方法が手軽です。すでに作った木は作り直されません。
rf = RandomForestClassifier(warm_start=True, oob_score=True, random_state=0, n_jobs=1)
for B in [10, 20, 40, 80, 160, 320]:
rf.set_params(n_estimators=B)
rf.fit(X_train, y_train) # 足りない分の木だけ追加で学習する
print(B, round(1 - rf.oob_score_, 4), round(1 - rf.score(X_test, y_test), 4))
B= 10 oob err 0.1496 test err 0.0519
B= 20 oob err 0.0684 test err 0.0370
B= 40 oob err 0.0414 test err 0.0296
B= 80 oob err 0.0294 test err 0.0278
B=160 oob err 0.0278 test err 0.0204
B=320 oob err 0.0247 test err 0.0241
OOBスコアとは(36.8%の根拠と自作での一致)
この節では、OOB スコアが何を測っているのか、なぜ約37%のデータが余るのか、本数が少ないと値がおかしくなる理由が分かります。
OOB(out-of-bag)スコアは、各行について「その行を学習に使わなかった木」だけで予測し、その正解率をまとめたものです。どの木から見てもテストデータと同じ扱いになるので、データを分けずに汎化性能を見積もれます。
n 件から n 回復元抽出したとき、ある行が1回も選ばれない確率は (1 − 1/n)^n で、n が大きくなると 1/e ≈ 0.368 に近づきます。乱数で確かめました。
| n | 実測(選ばれなかった割合) | (1 − 1/n)^n |
|---|---|---|
| 10 | 0.3570 | 0.3487 |
| 100 | 0.3618 | 0.3660 |
| 1,000 | 0.3673 | 0.3677 |
| 100,000 | 0.3682 | 0.3679 |
自作の森でも、木1本あたり平均 37.09%の行が OOB になり、OOB スコア 0.959799 が scikit-learn の oob_score_ と一致しました(前の節の出力)。
注意点は、本数が少ないと「どの木からも OOB にならない行」が残ることです。scikit-learn はそのとき警告を出し、その行は確率が0のまま計算されます。digits で1本のときの OOB スコアが 0.355 と極端に低いのはこのためで、テスト正解率(0.733)とかけ離れていました。
UserWarning: Some inputs do not have OOB scores. This probably means too few trees were used to compute any reliable OOB estimates.
手元では、乳がんでも digits でも、10本までは警告が出て、25本からは出ませんでした。また、OOB の各行は全体の約37%の木だけで予測されるので、本数が少ないうちは全部の木を使うテストより低く出ます。前の節の表でも、25本では OOB 0.933 に対しテスト 0.967 でした。100本以上では、digits で差が0.5ポイント以内、乳がんでは OOB のほうが1.2〜1.7ポイント高めでした。OOB とテストは別のデータでの見積もりなので、ぴったりは一致しません。
特徴量重要度とは(不純度ベースMDIとpermutationの違い)
この節では、ランダムフォレストの2種類の特徴量重要度が何を計算しているかを、自作のコードで scikit-learn と一致させて分かります。
不純度ベース(MDI)
その特徴量で分割したとき、ジニ不純度がどれだけ減ったかを、ノードの重み付きで全部の木について足し合わせる。feature_importances_ の中身。
学習のついでに出る・訓練データの値
permutation 重要度
学習済みのモデルに、ある列だけをシャッフルしたデータを入れ、正解率がどれだけ下がるかを測る。sklearn.inspection.permutation_importance。
予測し直す分の時間がかかる・テストデータで測れる
意味の違い
MDI は「木がその特徴量をどれだけ分割に使ったか」、permutation は「その特徴量の情報が無くなるとどれだけ当たらなくなるか」。
同じ順位になるとは限らない
MDI は、木の中のノードごとに「親の重み×不純度 − 左の子の重み×不純度 − 右の子の重み×不純度」を、そのノードで使った特徴量に足していき、木ごとに合計1に正規化してから木の間で平均します。
def my_mdi(forest, p):
per_tree = []
for est in forest.estimators_:
t = est.tree_; imp = np.zeros(p); w = t.weighted_n_node_samples
for nd in range(t.node_count):
l, r = t.children_left[nd], t.children_right[nd]
if l == -1: # 葉は分割しないので飛ばす
continue
imp[t.feature[nd]] += w[nd]*t.impurity[nd] - w[l]*t.impurity[l] - w[r]*t.impurity[r]
imp /= w[0]
if imp.sum() > 0:
imp /= imp.sum()
per_tree.append(imp)
m = np.mean(per_tree, axis=0)
return m / m.sum()
def my_permutation(est, X, y, n_repeats, random_state):
base = (est.predict(X) == y).mean()
seed = np.random.RandomState(random_state).randint(np.iinfo(np.int32).max + 1)
out = np.zeros((X.shape[1], n_repeats))
for j in range(X.shape[1]):
rs = np.random.RandomState(seed); Xp = X.copy(); idx = np.arange(len(X))
for k in range(n_repeats):
rs.shuffle(idx); Xp[:, j] = Xp[idx, j] # j 列だけ並べ替える
out[j, k] = base - (est.predict(Xp) == y).mean()
return out
my MDI max diff: 0.0
my perm max diff: 0.0
どちらも scikit-learn と差0で一致しました(permutation は scikit-learn と同じ乱数の使い方にしてあります)。仕組みが分かると、次の節の偏りがなぜ起きるかも見えてきます。
特徴量重要度の偏りを実測(ただの乱数が1位になる)
この節では、不純度ベースの特徴量重要度が、値の種類が多い特徴量や相関した特徴量でどう偏るかを、実際の数字で確かめられます。
値の種類が多い乱数が、効く特徴量より上に来る
架空のデータを1,000件作りました。ラベルは2値の特徴量 signal と80%だけ一致し(20%は雑音)、ほかの4列はラベルと無関係な乱数です。乱数は、値の種類が2・10・100の整数と、連続値(正規分布)の4種類にしました。森は300本、テストは3割です。
| 特徴量 | MDI(既定) | permutation(テスト) | permutation(訓練) |
|---|---|---|---|
| signal(2値・効く) | 0.280 | +0.270±0.025 | +0.357 |
| noise_cat2(2値の乱数) | 0.026 | +0.015±0.007 | +0.100 |
| noise_cat10(10種の乱数) | 0.120 | −0.002±0.013 | +0.173 |
| noise_cat100(100種の乱数) | 0.259 | +0.009±0.009 | +0.192 |
| noise_num(連続の乱数) | 0.315(1位) | +0.010±0.011 | +0.201 |
ラベルと無関係な連続の乱数が、MDI では1位(0.315)になり、本当に効いている signal(0.280)を上回りました。値の種類が多い特徴量ほど、分割できる位置が多く、深く育った木の下のほうで訓練データの雑音にたまたま合う分割を見つけやすいためです。公式の User Guide も、不純度ベースの重要度は「値の種類が多い特徴量を優遇する」「訓練データから計算されるので、未知のデータで役立つかを表すとは限らない」と注意しています。
permutation 重要度をテストデータで測ると、signal が 0.270 と大きく、乱数はほぼ0(−0.002〜+0.015)になりました。一方、permutation を訓練データで測ると、乱数も 0.17〜0.20 と大きく出ます。森が訓練データを丸暗記しているので、乱数の列を壊しても訓練データでは当たらなくなるからです。permutation はテストデータ(か検証用に分けたデータ)で測るのが基本です。
木を浅くすると偏りが小さくなる
同じデータで min_samples_leaf(葉に残す最小の件数)だけを変えました。
| min_samples_leaf | 訓練/テストの正解率 | signal の MDI | noise_num の MDI | noise_cat100 の MDI |
|---|---|---|---|---|
| 1(既定) | 1.000/0.770 | 0.280 | 0.315 | 0.259 |
| 5 | 0.781/0.807 | 0.515 | 0.192 | 0.172 |
| 20 | 0.771/0.810 | 0.754 | 0.095 | 0.075 |
| 50 | 0.771/0.810 | 0.799 | 0.075 | 0.055 |
葉を大きくすると雑音への当てはめが減り、signal の MDI が 0.280 から 0.799 に上がって順位が正しくなりました。テストの正解率も 0.770 から 0.810 に上がっています。このデータのように雑音が多いときは、既定の「深さ無制限・葉1件」が精度の面でも重要度の面でも不利になります。なお max_features を None(全特徴量)にしても、noise_num の MDI は 0.310 で偏りは残りました。
同じ情報の特徴量が2つあると、重要度が分け合われる
乳がんデータの「worst radius」に、ごく小さな雑音を足した複製の列を加えて学習し直しました。
| 条件 | worst radius の MDI | 複製の MDI | worst radius の permutation | 複製の permutation |
|---|---|---|---|---|
| 元の30列 | 0.133 | ― | 0.021 | ― |
| 複製を足した31列 | 0.113 | 0.099 | 0.007 | 0.008 |
MDI は2つに分け合われ、permutation は2つとも3分の1ほどに下がりました。片方をシャッフルしても、もう片方が同じ情報を持っているので正解率があまり下がらないためです。公式の User Guide(Permutation feature importance)も、相関した特徴量では両方の重要度が低く出ると注意し、相関した特徴量をまとめて各グループから1つだけ残す方法を挙げています。乳がんデータは、相関係数の絶対値が0.9を超える特徴量の組が21組あり(worst radius と worst perimeter は 0.994)、そもそも「どれが重要か」を1列ずつで言いにくいデータです。相関の強い特徴量はまとめて1つのグループとして扱う(グループごと並べ替える・代表の1列を残す)と解釈しやすくなります。
実在のデータでも確かめました。load_diabetes(特徴量10個)に乱数の列を1つ足して回帰の森を作ると、乱数の MDI は 0.048 で11列中6位となり、年齢(age)・コレステロール系の s1・s2・s4 より上でした。逆に2値の性別(sex)は MDI 0.008 で最下位でしたが、テストでの permutation は +0.008±0.003 と、乱数(−0.009)より上でした。値の種類が少ない特徴量は、MDI では低く見えやすいということです。
乳がんデータに同じ乱数の列を足した場合は、乱数の MDI は 0.003(32列中30位)にとどまりました。効く特徴量が強いデータでは偏りは目立ちません。偏りが問題になるのは、信号が弱く雑音が多い、つまり重要度を知りたくなる場面そのものです。
特徴量重要度の読み方
手元では連続の乱数が MDI 1位、permutation(テスト)ではほぼ0だった
なお、どちらの重要度も「モデルがどの特徴量を頼りにしているか」であって、因果関係ではありません。表のデータの前処理や列の扱いは、関連記事「pandasの使い方 逆引き81選|pandas 3.0の実行結果つき」で扱っています。
ランダムフォレストの回帰と外挿の弱点
この節では、回帰でのランダムフォレストの使い方と、訓練データの範囲の外を予測できないという弱点が分かります。
回帰は RandomForestRegressor を使い、木の予測の平均を答えにします。max_features の既定は分類が "sqrt"、回帰が 1.0(全特徴量)で、手元の 1.9.1 でも確かめました。公式の User Guide は、これらを「経験的によい既定値」としています。
load_diabetes の5分割交差検証の決定係数(R²)は次のとおりでした。
| モデル | R²(平均±標準偏差) |
|---|---|
| 決定木1本 | −0.096±0.170 |
| ランダムフォレスト(既定・300本) | 0.425±0.051 |
| ランダムフォレスト(min_samples_leaf=5・max_features=0.33) | 0.453±0.060 |
| 線形回帰 | 0.482±0.049 |
1本の木はマイナス(平均で予測するより悪い)でしたが、森にすると0.425まで上がりました。ただしこのデータは関係がほぼ直線的なので、線形回帰のほうが良い結果でした。ランダムフォレストは何にでも勝つ手法ではありません。
もう1つの弱点が外挿です。y = 2x(雑音つき)を x=0〜10 の200点で学習させ、範囲の外を予測させました。
x= 5.0 true 10.0 rf 10.36 lr 10.04
x= 9.9 true 19.8 rf 20.42 lr 19.79
x= 12.0 true 24.0 rf 20.65 lr 23.97
x= 15.0 true 30.0 rf 20.65 lr 29.94
x= 20.0 true 40.0 rf 20.65 lr 39.90
rf predict range on [0,100]: 0.69 20.65 train y max 20.98
x が10を超えると、森の予測は 20.65 で止まりました。x=0〜100 のどこを入れても予測は 0.69〜20.65 の間で、訓練データの y の最大値(20.98)を超えません。木の葉の値は訓練データの平均なので、見たことのない範囲の値は出せないのです。売上の伸びや気温の上昇のように、将来の値が過去の範囲を超えるものを予測するときは、差分(前日比)を目的変数にする・線形モデルと組み合わせる、といった工夫が要ります。
ハイパーパラメータの決め方(既定値と実測)
この節では、ランダムフォレストの主な引数の意味と既定値、変えたときに精度と木の大きさがどう変わるかが分かります。
既定値は scikit-learn 1.9.1 の inspect.signature で出したものです。
| 引数 | 既定値 | 意味と目安 |
|---|---|---|
| n_estimators | 100 | 木の本数。OOB が横ばいになる本数まで。増やして悪くなることはない |
| max_features | "sqrt"(回帰は 1.0) | 分割ごとの候補の数。最も効く引数。"log2"・0.3〜0.5 も試す。1.1 で既定が "auto" から "sqrt" に変わり、"auto" は今は指定するとエラー |
| max_depth | None | 深さの上限。None は葉が純粋になるまで育てる。サイズを抑えたいときに使う |
| min_samples_leaf | 1 | 葉の最小件数。雑音が多いデータでは5〜20で精度も重要度も安定することがある |
| max_samples | None | 1本の木に使う件数(割合も可)。大きなデータで学習を速くしたいとき |
| bootstrap / oob_score | True / False | 復元抽出するか / OOB スコアを計算するか。oob_score は bootstrap=True のときだけ |
| class_weight | None | 不均衡なクラスの重み。"balanced" と、木ごとに計算する "balanced_subsample" |
| criterion | "gini" | 分割の基準。"entropy" と "log_loss" は同じ計算 |
| n_jobs / random_state | None / None | 並列数(-1 で全コア)/ 乱数の種。再現したいなら固定する |
digits(200本)で1つずつ変えた結果です。「ノード数」は木1本あたりの平均で、モデルのサイズと予測時間の目安になります。
| 変えた引数 | OOB スコア | テスト正解率 | 学習時間 | ノード数/木 |
|---|---|---|---|---|
| 既定 | 0.973 | 0.978 | 0.88秒 | 315 |
| max_depth=10 | 0.975 | 0.980 | 0.85秒 | 284 |
| max_depth=5 | 0.936 | 0.943 | 0.75秒 | 59 |
| max_depth=3 | 0.878 | 0.896 | 0.58秒 | 15 |
| min_samples_leaf=5 | 0.959 | 0.961 | 0.79秒 | 157 |
| min_samples_leaf=20 | 0.932 | 0.937 | 0.78秒 | 59 |
| max_samples=0.3 | 0.968 | 0.978 | 0.63秒 | 162 |
| max_features="log2" | 0.975 | 0.974 | 0.83秒 | 352 |
| max_features=0.5 | 0.963 | 0.978 | 1.79秒 | 220 |
| criterion="entropy" | 0.974 | 0.978 | 1.04秒 | 293 |
digits は雑音が少ないので、深く育てる既定が強く、max_depth=10 で精度を保ったままノードを1割減らせました。max_samples=0.3 は、木1本に使うデータを3割にしてもテスト正解率が同じで、学習時間とノード数が減りました。前の節の雑音の多い架空データでは逆に min_samples_leaf を大きくしたほうが良かったように、最適な値はデータで変わります。調整は max_features → min_samples_leaf(か max_depth)→ n_estimators の順に、OOB スコアか交差検証で比べるのが手堅い方法です。
n_jobs は、このマシン(2コア・共有)で300本の学習が n_jobs=1 で 1.21秒、2 で 1.02秒でした。公式の User Guide にあるとおり、並列にしても速さはコア数に比例しません。
scikit-learn 1.9.1 のランダムフォレストは、欠損値(NaN)をそのまま受け取れます(公式の API リファレンスに「native support for missing values」と明記)。乳がんデータの値の10%を NaN にしても、エラーにならず学習でき、テスト正解率は 0.942(欠損なしは 0.953)でした。以前の版では NaN を入れるとエラーになったので、版を確かめてから頼るのが安全です。
よくあるエラーとつまずき(原文つき)
この節では、ランダムフォレストを使うときに出るエラーと警告の原文と、その直し方が分かります。
すべて手元の scikit-learn 1.9.1 で実際に出したものです。
| 原文(抜粋) | 原因 | 直し方 |
|---|---|---|
| InvalidParameterError: The 'max_features' parameter of RandomForestClassifier must be an int in the range [1, inf), a float in the range (0.0, 1.0], a str among {'log2', 'sqrt'} or None. Got 'auto' instead. | 以前の版の記事のコードの max_features="auto" | 分類は "sqrt"、回帰は 1.0 に書き換える |
| ValueError: Out of bag estimation only available if bootstrap=True | bootstrap=False で oob_score=True | bootstrap=True に戻すか、交差検証で評価する |
| ValueError: `max_sample` cannot be set if `bootstrap=False`. | 復元抽出しないのに max_samples を指定 | max_samples=None にする |
| ValueError: could not convert string to float: 'a' | 文字列の列をそのまま渡した | OrdinalEncoder などで数値にする |
| ValueError: Unknown label type: continuous. | 連続値の目的変数を Classifier に渡した | RandomForestRegressor を使う |
| DataConversionWarning: A column-vector y was passed when a 1d array was expected. | y が (n, 1) の2次元 | y.ravel() で1次元にする |
| UserWarning: Some inputs do not have OOB scores. | 木が少なく OOB にならない行がある | 本数を増やす(手元では25本以上で消えた) |
| ValueError: n_estimators=20 must be larger or equal to len(estimators_)=50 when warm_start==True | warm_start で本数を減らした | 新しく作り直すか、本数を増やす方向だけにする |
| UserWarning: Warm-start fitting without increasing n_estimators does not fit new trees. | warm_start のまま同じ本数で再学習 | データを変えたなら warm_start=False で作り直す |
| ValueError: X has 3 features, but RandomForestClassifier is expecting 4 features as input. | 学習時と予測時の列数が違う | 前処理を Pipeline にまとめ、同じ列で予測する |
| UserWarning: X does not have valid feature names, but RandomForestClassifier was fitted with feature names | DataFrame で学習し numpy 配列で予測した | 予測にも同じ列名の DataFrame を渡す |
| ValueError: Input X contains infinity or a value too large for dtype('float32'). | inf が入っている(NaN は通るが inf は通らない) | inf を NaN か有限の値に置き換える |
| InvalidParameterError: The 'criterion' parameter of RandomForestRegressor must be a str among {'squared_error', 'absolute_error', 'poisson'}. Got 'gini' instead. | 分類用の基準を回帰に指定 | 回帰は "squared_error" などを使う |
エラーにはならないつまずきもあります。
- OOB スコアだけを見て、特徴量や引数の選び直しを何度もくり返す:OOB も、何度も見て選べば楽観的になります。最後の評価は手を付けていないテストデータで行います。
- random_state を固定せずに比べる:森の乱数による揺れを、引数の効果と取り違えます。手元の digits(200本)で random_state だけを8通り変えると、テスト正解率は 0.970〜0.978 と0.7ポイント動きました。
- 深さ無制限のまま大きなデータで学習して、モデルが数GBになる:公式の API リファレンスの Notes も、既定値は「完全に育った剪定なしの木」になりメモリを食うので、max_depth や min_samples_leaf で大きさを抑えるよう書いています。
XGBoost・勾配ブースティングとの判断表(ランダムフォレストを選ぶとき)
この節では、ランダムフォレストと勾配ブースティング(XGBoost・LightGBM・HistGradientBoosting)のどちらを使うか、ほかの手法とどう使い分けるかを判断できます。
XGBoost はこのマシンに入っていないため、scikit-learn の HistGradientBoostingClassifier(LightGBM と同じヒストグラム型の勾配ブースティング)で比べました。どれも既定の設定で、時間は1回の学習です。
| データ | ランダムフォレスト(300本) | Extra Trees | HistGradientBoosting | ロジスティック回帰 |
|---|---|---|---|---|
| breast_cancer | 0.953(0.92秒) | 0.942(0.55秒) | 0.942(0.29秒) | 0.959(0.01秒) |
| digits | 0.978(1.67秒) | 0.987(1.26秒) | 0.982(4.21秒) | 0.972(0.05秒) |
| make_classification 5,000×20 | 0.902(6.18秒) | 0.909(1.59秒) | 0.914(0.58秒) | 0.853(0.02秒) |
件数が増えると、勾配ブースティングは精度でも速さでも森を上回りました(5,000件で 0.914・0.58秒 対 0.902・6.18秒)。一方、乳がんのように小さく直線的なデータでは、標準化したロジスティック回帰がいちばん良く、いちばん速い結果でした。XGBoost の使い方と LightGBM・ランダムフォレストとの比較は、別記事「XGBoostとは・使い方|Pythonで分類・回帰とLightGBM比較を実測」(公開準備中)で扱います。
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 表のデータで、まず基準となる精度がほしい | ランダムフォレスト | 既定値のままでも大きく外さず、標準化も要らない。OOB で検証用データも要らない |
| 精度を最後の1ポイントまで上げたい・件数が多い | 勾配ブースティング | 手元の5,000件で精度・速さとも上。ただし学習率・木の数・深さの調整と early stopping が要る |
| 調整に時間をかけられない・過学習が怖い | ランダムフォレスト | 木を増やしても過学習しない。ブースティングは本数を増やすと過学習しうる |
| 関係が直線的・係数で説明したい | 線形回帰・ロジスティック回帰 | 手元の diabetes・乳がんでは森より良かった |
| 訓練の範囲の外を予測する(将来の伸び) | 線形モデルか、差分を目的変数にする | 森は訓練データの y の範囲を超えられない |
| 1件ずつ低遅延で答える API | 本数・深さを抑えた森か、浅い木のブースティング | 手元の200本で1件 20ms。予測時間は本数×深さで増える |
| どの特徴量が効いているか説明したい | どれでもよいが、permutation で測る | 不純度ベースは乱数を1位にすることがある |
| 画像・文章・音声 | ニューラルネットワーク | 生の画素や文字の並びから特徴を作るのは木が苦手 |
画像や文章に使うニューラルネットワークの仕組みは、関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。
使う前に確かめる点をまとめます。
学び始めの順番は次のとおりです。
- 1決定木を1本作り、訓練とテストの差を見る深さ無制限で訓練1.000・テスト0.906 になる様子
- 2RandomForestClassifier を既定で動かし、OOB とテストを比べる最初の5分のコード
- 3バギングを自作して一致させるブートストラップ・重み・確率の平均・OOB の4つ
- 4max_features と本数を動かして、相関と精度の関係を見るρσ² の項が森の限界を決める
- 5特徴量重要度を2種類で出し、乱数の列で確かめる解釈を誤らないための習慣
- 6勾配ブースティングと同じデータで比べる精度・時間・調整の手間で選ぶ
ここまでの内容は、ディープラーニングの資格試験の範囲の外側にある「表のデータの機械学習」の基礎でもあります。資格の勉強と合わせて学ぶ場合は、関連記事「E資格とは|試験内容・合格率・G検定との違いとNumPyで解く要点」で扱っています。機械学習の全体像(教師あり・教師なし・強化学習)は別記事「機械学習とは|種類・ディープラーニングとの違いを実行結果で解説」(公開準備中)、正解率以外の評価の指標は「Precision(適合率)・再現率・正解率・F1の違い|陽性1%で実測」(公開準備中)で扱います。
よくある質問
この節では、ランダムフォレストについてよく聞かれる質問への答えが分かります。
Q. ランダムフォレストとは、ひと言でいうと何ですか。
少しずつ違うデータと特徴量で作ったたくさんの決定木の、平均をとる手法です。1本の木の「揺れやすさ」を、似ていない木を集めて打ち消します。
Q. 決定木との違いは何ですか。
決定木は1本で、訓練データに合わせすぎやすい手法です。ランダムフォレストはそれを多数集めます。手元の乳がんデータでは、1本 0.926 に対して森 0.965(5分割交差検証)でした。代わりに、1本の木のように分割の流れを読んで説明することはできなくなります。
Q. なぜ「ランダム」なのですか。
木ごとに学習データを無作為に選び直すことと、分割ごとに候補の特徴量を無作為に選ぶことの2つの偶然を入れるからです。偶然で木どうしを似せないことが、平均の効果を大きくします。
Q. 木は何本にすればよいですか。
OOB スコアが横ばいになる本数です。手元の乳がんと digits では100〜200本でほぼ横ばいでした。増やしても精度は下がりませんが、学習・予測の時間とモデルのサイズは本数に比例して増えます。
Q. 標準化や正規化は要りますか。
要りません。木は特徴量ごとのしきい値で分けるだけなので、単位や大きさを変えても分け方は同じです。ただし、ロジスティック回帰など別の手法と同じ Pipeline で比べるときは、そちらには標準化を入れます。
Q. 過学習しないのですか。
木の本数を増やすことでは過学習しません。ただし1本1本の木は訓練データを丸暗記しており、訓練の正解率は1.000になります。雑音の多いデータでは、手元の例のように min_samples_leaf を大きくするとテストの正解率が 0.770 から 0.810 に上がることがあります。
Q. 特徴量重要度は信用できますか。
既定の feature_importances_(不純度ベース)は、値の種類が多い特徴量を高く見せます。手元では、ラベルと無関係な連続の乱数が1位になりました。テストデータでの permutation 重要度と合わせて読み、乱数の列を足して基準線にすると安全です。
Q. OOB スコアとテストデータの正解率、どちらを使えばよいですか。
調整中の目安は OOB、最後の報告は手を付けていないテストデータです。OOB は木が少ないと低く出て、手元の digits の25本では OOB 0.933・テスト 0.967 と差がありました。100本以上では差は2ポイント以内に収まりました。
Q. 欠損値があっても使えますか。
scikit-learn 1.9.1 では NaN をそのまま受け取れます。手元では値の10%を NaN にしても学習でき、正解率は 0.942(欠損なし 0.953)でした。inf はエラーになるので置き換えます。
Q. XGBoost とどちらを使えばよいですか。
まず基準を早く出したいならランダムフォレスト、件数が多く精度を詰めたいなら勾配ブースティングです。手元の5,000件のデータでは HistGradientBoosting が精度 0.914・0.58秒で、森の 0.902・6.18秒を上回りました。
Q. 回帰にも使えますか。
使えます。RandomForestRegressor で、木の予測の平均を答えにします。既定の max_features は 1.0(全特徴量)です。訓練データの目的変数の範囲を超える値は予測できない点に注意します。
まとめ
この節では、記事全体の要点が分かります。
- ランダムフォレストは、復元抽出と分割ごとの特徴量のランダム選択で作った多数の決定木の平均。scikit-learn は確率の平均で分類し、numpy で自作すると予測確率・OOB・重要度まで差0で一致した。
- 強さの理由は「似ていない木を集める」こと。平均の分散は ρσ² + (1−ρ)σ²/B で、max_features を減らすと相関 ρ が 0.828 から 0.794 に下がり、森の正解率は 0.924 から 0.953 に上がった。
- 木の本数は OOB が横ばいになるところまで。手元では100〜200本で頭打ちになり、時間とサイズ(500本で22MiB)は本数に比例した。OOB は木が少ないと警告が出て低く出る。
- 不純度ベースの特徴量重要度は偏る。ラベルと無関係な連続の乱数が MDI 1位(0.315)になり、テストでの permutation ではほぼ0だった。相関した列は重要度を分け合う。
- 万能ではない。直線的なデータでは線形モデル、件数が多く精度を詰めるなら勾配ブースティングが上回り、訓練の範囲の外は予測できない(手元の例で 20.65 で止まった)。
参考・出典
この節では、この記事で根拠にした論文と公式ドキュメントが分かります(確認日 2026年11月5日)。
- Leo Breiman「Random Forests」Machine Learning 45, 5–32(2001)
- Leo Breiman「Bagging Predictors」Machine Learning 24, 123–140(1996)
- Tin Kam Ho「The Random Subspace Method for Constructing Decision Forests」IEEE Transactions on Pattern Analysis and Machine Intelligence 20(8)(1998)
- Trevor Hastie, Robert Tibshirani, Jerome Friedman『The Elements of Statistical Learning』第2版 15章「Random Forests」
- scikit-learn 1.9.1 User Guide「Ensembles: Gradient boosting, random forests, bagging, voting, stacking」:https://scikit-learn.org/stable/modules/ensemble.html
- scikit-learn 1.9.1 API リファレンス「RandomForestClassifier」:https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html
- scikit-learn 1.9.1 User Guide「Permutation feature importance」:https://scikit-learn.org/stable/modules/permutation_importance.html
- scikit-learn 1.9.1 のソースコード(sklearn/ensemble/_forest.py・sklearn/inspection/_permutation_importance.py)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「ランダムフォレストとは|バギングを自作し特徴量重要度の偏りを実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。