- AI ⚙️
決定木とは|ジニ不純度・剪定・過学習をPythonで自作して実測
結論
決定木とは、「契約月数は7か月以下か」のような「はい・いいえ」で答えられる質問でデータを次々に分け、最後にたどり着いた葉で分類や数値を予測する機械学習の手法です。質問は、分けたあとの混ざり具合(ジニ不純度など)がいちばん小さくなるものが自動で選ばれます。
この記事でわかること
- ジニ不純度・エントロピーの手計算と、分割の選び方
- numpyで自作した木とscikit-learnの分割が一致するかの照合
- 深さ・剪定(ccp_alpha等)で過学習がどう変わるかの実測
この記事で実際に動かしたもの:架空の会員10人のデータでの手計算、numpyで書いたCART(約60行)、scikit-learn 1.9.1 の DecisionTreeClassifier・DecisionTreeRegressor(同梱データ iris・wine・breast_cancer・digits・diabetes と make_moons)。
下の図は、この記事の2節で手計算する「会員の解約予測」を、scikit-learn が実際に学習した木です。
flowchart TD
A{"契約月数 <= 7?<br>10人 解約5・継続5"} -->|はい| B["葉: 解約<br>4人 すべて解約"]
A -->|いいえ| C{"利用回数 <= 3?<br>6人 解約1・継続5"}
C -->|はい| D["葉: 解約<br>1人"]
C -->|いいえ| E["葉: 継続<br>5人"]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| 決定木とは | 質問の分岐でデータを分けて予測する手法。分類にも回帰にも使える | 決定木とは |
| 決定木分析(ビジネス)との違い | 意思決定の決定木は人が期待値で選ぶ図。機械学習の決定木はデータから分岐を学ぶ | 決定木とは |
| ジニ不純度とは | 1−(各クラスの割合の2乗の合計)。0なら1種類だけ、2クラスの最大は0.5 | ジニ不純度とエントロピーの計算 |
| 決定木 python の書き方 | DecisionTreeClassifier を fit し、export_text で木を文字で読む | 最初の5分 |
| 決定木はなぜ過学習するか | 深さを制限しないと訓練データを100%当てるまで分け続ける | 決定木の深さと過学習 |
| 剪定のやり方 | max_depth・min_samples_leaf で事前に止めるか、ccp_alpha で後から刈る | 剪定 |
| 標準化は要るか | 要らない。値の大小の順番だけで分けるので、結果が1件も変わらなかった | スケールが要らない理由 |
| 毎回結果が変わる | 同点の分割がランダムに選ばれるため。random_state を固定する | 分割の選び方と同点 |
確認環境:Ubuntu 24.04.5 LTS(CPU 2コア)で、Python 3.13.18、scikit-learn 1.9.1、numpy 2.5.3、scipy 1.18.1、pandas 3.0.5、matplotlib 3.11.2 を使いました。データは scikit-learn に同梱の load_iris・load_wine・load_breast_cancer・load_digits・load_diabetes、make_moons・make_classification で作ったもの、架空の会員データだけで、ネットからのデータ取得はしていません。numpy だけで CART(分類はジニ不純度とエントロピー、回帰は二乗誤差)を書き、scikit-learn の木とノードごとに照合しました。計算はすべて OPENBLAS_NUM_THREADS=1 の1スレッドです。アルゴリズムの説明(CART・欠損値・コスト複雑度剪定・計算量)、random_state の意味、特徴量重要度の注意は scikit-learn 1.9.1 の公式ドキュメントで確かめました。図は mermaid-cli 11.16.0 で描画を確かめています(確認日 2026年11月5日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「決定木とは|ジニ不純度・剪定・過学習をPythonで自作して実測」
決定木とは(意味と、決定木分析との違い)
この節では、決定木の意味と、ビジネスで使う「決定木分析」と機械学習の決定木の違いが分かります。
決定木(decision tree)は、データを「特徴量 ≤ しきい値か」という質問で2つに分ける操作を繰り返し、木の形のルールを作る機械学習の手法です。答えがクラス(解約する・しない、品種A・B・C)なら分類木、数値(売上、病気の進行度)なら回帰木と呼びます。scikit-learn の公式ドキュメントは、scikit-learn の決定木を「CART アルゴリズムを最適化したもの」と説明しています。
「決定木」という言葉は、機械学習の外でも使われます。検索すると両方が混ざって出てくるので、最初に分けておきます。
| 場面 | 「決定木」の意味 | 分岐を決めるもの | 例 |
|---|---|---|---|
| ふだん・経営の意思決定 | 選択肢と起こりうる結果を枝分かれで描いた図(ディシジョンツリー) | 人が選択肢と確率・金額を書き、期待値で選ぶ | 新サービスを出すか:成功確率0.4で+1,000万円、失敗0.6で−300万円なら期待値は+220万円 |
| マーケティングの決定木分析 | 顧客を属性で分け、どの層が買うかを見る分析 | データから自動で(中身は機械学習の決定木) | 年代と来店回数で、購入率が高い層を見つける |
| 機械学習・IT | データから「はい・いいえ」の分岐を学習して予測するモデル | 不純度(ジニ・エントロピー・二乗誤差)が最も下がる分割 | DecisionTreeClassifier で解約を予測 |
| ふだんの「剪定」 | 庭木の枝を切って形を整えること | 人が見て切る | 松の剪定 |
| 機械学習の「剪定」 | 育ちすぎた木の枝(分割)を減らし、過学習を抑えること | 深さの上限・葉の最小件数・ccp_alpha | 葉16枚の木を葉6枚にしてテストの正解率が上がった(7節) |
この記事は、3行目以降の「機械学習の決定木」を扱います。機械学習全体の中での位置づけ(教師あり学習の1つ)や、ニューラルネットワークとの違いは、関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。
読める
学習した結果が if 文の集まりになる。export_text でそのまま文字にできる。
説明が要る業務向き
前処理が少ない
標準化が要らない。欠損値も scikit-learn 1.9 ではそのまま学習できる。
文字の列は数値に変える
過学習しやすい
放っておくと訓練データを100%当てるまで分け続ける。剪定が前提。
7・8節で実測
不安定
同点の分割の選び方やデータの少しの違いで、木の形が変わる。
束ねるとランダムフォレスト
最初の5分:決定木をPythonで動かす(scikit-learn)
この節では、決定木 python の最小のコードと、学習した木を文字で読む方法が分かります。
scikit-learn に同梱のアヤメのデータ(iris。花びら・がくの長さと幅の4列から3品種を当てる)で、深さ3の木を作ります。
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, export_text
iris = load_iris()
clf = DecisionTreeClassifier(max_depth=3, random_state=0)
clf.fit(iris.data, iris.target)
print(export_text(clf, feature_names=list(iris.feature_names), show_weights=True))
print(clf.predict_proba([[6.0, 2.9, 4.9, 1.6]]), clf.predict([[6.0, 2.9, 4.9, 1.6]]))
実行結果です。weights は、その葉に落ちた訓練データの品種ごとの件数です。
|--- petal width (cm) <= 0.80
| |--- weights: [50.00, 0.00, 0.00] class: 0
|--- petal width (cm) > 0.80
| |--- petal width (cm) <= 1.75
| | |--- petal length (cm) <= 4.95
| | | |--- weights: [0.00, 47.00, 1.00] class: 1
| | |--- petal length (cm) > 4.95
| | | |--- weights: [0.00, 2.00, 4.00] class: 2
| |--- petal width (cm) > 1.75
| | |--- petal length (cm) <= 4.85
| | | |--- weights: [0.00, 1.00, 2.00] class: 2
| | |--- petal length (cm) > 4.85
| | | |--- weights: [0.00, 0.00, 43.00] class: 2
[[0. 0.97916667 0.02083333]] [1]
読み方はそのまま if 文です。「花びらの幅が0.8cm以下なら品種0(setosa)。0.8cmより大きく1.75cm以下で、花びらの長さが4.95cm以下なら品種1」。predict_proba の 0.979 は、たどり着いた葉の件数の割合(47÷48)です。決定木の「確率」は葉の中の割合でしかないため、葉の件数が少ないと 1.0 や 0.0 のような極端な値になります。
この木で使われたのは花びらの幅と長さの2列だけで、feature_importances_ は petal width 0.9461、petal length 0.0539、がくの2列は 0 でした。
分類なら DecisionTreeClassifier、数値なら DecisionTreeRegressor
目的変数が連続値なのに Classifier を使うと「Unknown label type: continuous」で止まる(11節)。
random_state を整数で固定する
固定しないと、同点の分割の選び方が実行ごとに変わり、木の形と精度がぶれる(4節)。
max_depth などで大きさを決める
何も指定しないと葉が純粋になるまで育つ。最初は max_depth=3〜5 で読める大きさにする。
export_text・plot_tree で木を読む
文字なら export_text、図なら plot_tree(matplotlib が要る)や export_graphviz(DOT 形式の文字列)。
Jupyter や Colab で試す場合、図の表示は matplotlib を使います。Colab の始め方は関連記事「Google Colabとは|使い方・GPU・料金・制限とエラーの直し方」で扱っています。
決定木の仕組みと用語(ルート・ノード・葉・深さ)
この節では、決定木の図や出力に出てくる用語と、学習の手順が分かります。
| 用語 | 意味 | scikit-learn での見方 |
|---|---|---|
| ルートノード(根) | いちばん上の、最初の質問。全データが入る | tree_.feature[0]・tree_.threshold[0] |
| 内部ノード(分岐) | 途中の質問。左が「はい(≤)」、右が「いいえ(>)」 | tree_.children_left が −1 でないノード |
| 葉(リーフ) | それ以上分けない終点。ここの多数決(回帰は平均)が予測 | get_n_leaves()・apply(X) で葉の番号 |
| 深さ | 根から最も遠い葉までの質問の数 | get_depth() |
| 不純度 | ノードの中の混ざり具合。0なら1種類だけ | tree_.impurity |
| しきい値 | 分ける値。隣り合う2つの値のちょうど中間 | 2.0 と 4.0 の間で分けるなら 3.0 |
学習の手順は次の繰り返しです。
- 今のノードのデータで、すべての列・すべての区切り位置を試す。
- 分けたあとの左右の不純度を件数で重み付けして足し、それがいちばん小さい(=不純度の減り方が最大の)分割を選ぶ。
- 左右の子ノードで同じことをする。葉が純粋になる、深さの上限に届く、件数が下限を割る、のどれかで止める。
各ノードで「今いちばん良い分割」を選ぶだけで、先のことは考えません(貪欲法)。scikit-learn の公式ドキュメントは、最適な決定木を求める問題は NP 完全であり、実用のアルゴリズムは貪欲法のような発見的な方法なので、全体として最適な木は保証されないと書いています。予測は根から葉まで質問をたどるだけなので、かかる時間は深さに比例します。
ジニ不純度とエントロピーの計算(手計算)
この節では、ジニ不純度とエントロピーの式と、10人のデータでの手計算が分かります。
架空の会員10人のデータです。「月の利用回数」と「契約月数」から「解約したか」を当てます。
| 会員 | 利用回数(月) | 契約月数 | 解約 |
|---|---|---|---|
| A | 1 | 3 | する |
| B | 2 | 14 | する |
| C | 2 | 5 | する |
| D | 3 | 2 | する |
| E | 4 | 20 | しない |
| F | 6 | 4 | する |
| G | 7 | 18 | しない |
| H | 8 | 9 | しない |
| I | 9 | 24 | しない |
| J | 10 | 12 | しない |
ジニ不純度の式
クラス k の割合を p_k とすると、ジニ不純度 = 1 − Σ p_k² です。「このノードから2件を無作為に(戻しながら)取り出したとき、クラスが違う確率」と読めます。
- 10人全体:解約5・継続5 なので
1 − (0.5² + 0.5²) = 0.5。2クラスでの最大値です。 - 全員が解約:
1 − 1² = 0。混ざりがありません。 - 解約1・継続5:
1 − ((1/6)² + (5/6)²) = 1 − 26/36 = 0.2778。
エントロピーの式
エントロピー = −Σ p_k log₂ p_k です。2クラスで半々なら1.0、1種類だけなら0です。解約1・継続5のノードは −(1/6·log₂(1/6) + 5/6·log₂(5/6)) = 0.650 になります。エントロピーの意味や交差エントロピーとの関係は、関連記事「エントロピーとは|意味・計算・交差エントロピーをPythonで実演」で扱っています。
分割の良さ(情報利得)
「契約月数 ≤ 7」で分けると、左は A・C・D・F の4人(全員解約)、右は6人(解約1・継続5)です。
分割後のジニ = 4/10 × 0 + 6/10 × 0.2778 = 0.1667
ジニの減少量 = 0.5 − 0.1667 = 0.3333
分割後のエントロピー = 4/10 × 0 + 6/10 × 0.650 = 0.390
情報利得 = 1.0 − 0.390 = 0.610
Python で全候補を計算し、分割後のジニが小さい順に並べた上位です(左右は[継続, 解約]の件数)。
列 しきい値 左の件数 右の件数 左 右 分割後ジニ ジニ減少 情報利得
契約月数 7.0 4 6 [0, 4] [5, 1] 0.1667 0.3333 0.610
利用回数 3.5 4 6 [0, 4] [5, 1] 0.1667 0.3333 0.610
利用回数 6.5 6 4 [1, 5] [4, 0] 0.1667 0.3333 0.610
利用回数 2.5 3 7 [0, 3] [5, 2] 0.2857 0.2143 0.3958
契約月数 4.5 3 7 [0, 3] [5, 2] 0.2857 0.2143 0.3958
利用回数 5.0 5 5 [1, 4] [4, 1] 0.3200 0.1800 0.2781
scikit-learn に同じデータを渡すと、tree_.impurity は根から順に [0.5, 0, 0.2778, 0, 0]、エントロピーでは [1.0, 0, 0.65, 0, 0] で、手計算と一致しました。木の形は冒頭の図のとおりで、右の6人を「利用回数 ≤ 3.0」(6人の利用回数 2・4・7・8・9・10 のうち 2 と 4 の中間)で分けています。
ジニとエントロピーはどちらを使うか
このデータでは、どちらでも同じ木になりました。下の8節の照合でも、深さ3では件数・精度とも大きな差は出ていません。既定値は criterion="gini" で、対数を使わない分だけ計算が軽く、迷ったらジニで始めるのが一般的です。scikit-learn 1.9 では "log_loss" も指定でき、"entropy" と同じ(シャノンの情報利得)と公式ドキュメントに書かれています。手元でも log_loss の不純度は [1., 0., 0.] でエントロピーと同じ値でした。
分割の選び方と同点(random_stateで結果が変わる理由)
この節では、決定木の結果が実行ごとに変わる理由と、それがどれくらい精度に響くかが分かります。
上の表を見ると、分割後のジニが 0.1667 で並ぶ分割が3つあります。同点のとき、scikit-learn は列の順番をランダムに並べ替えて先に見つかったものを選びます。公式ドキュメントの random_state の説明には「max_features=n_features でも、複数の分割で基準の改善が同じ場合はランダムに1つ選ばれるため、最良の分割は実行ごとに変わりうる。決定的な動きにするには random_state を整数に固定する」とあります。
random_state を 0〜99 に変えて、根の分割を数えた結果です。
| 根の分割 | 選ばれた回数(100回中) | 理由 |
|---|---|---|
| 契約月数 ≤ 7.0 | 51 | 先に契約月数の列を見た回 |
| 利用回数 ≤ 3.5 | 49 | 先に利用回数の列を見た回 |
| 利用回数 ≤ 6.5 | 0 | 同じ列の中では小さいしきい値が先に見つかる |
同点は小さな例だけの話ではありません。wine(ワイン178本・13列)を7:3に分け、深さ3の木を random_state 0〜49 で50回学習すると、テストの正解率は 0.852〜0.963 の7通りに分かれました。データも設定も同じで、違うのは同点の選び方だけです。
モデルを比べるときは、random_state を固定するだけでなく、複数の値で学習して平均とばらつきを見ると、たまたま良い(悪い)木を選ぶことを避けられます。木を何本も作って平均するのがランダムフォレストの考え方です。
numpyで決定木を自作してscikit-learnと照合する
この節では、決定木の中身を numpy で書き、scikit-learn と同じ分割を選んでいるかを確かめた結果が分かります。
分類(ジニ・エントロピー)と回帰(二乗誤差)に対応した CART です。列ごとに値を並べ替え、累積和で「左に k 件」のときの左右の件数を一度に計算します。scikit-learn は内部で X を float32 に変換するため、自作でも同じ変換をしてからしきい値を計算しています。
import numpy as np
class MyTree:
"""numpy だけで書いた CART(分類: gini/entropy、回帰: 二乗誤差)"""
def __init__(s, criterion="gini", max_depth=None, min_samples_leaf=1, task="clf"):
s.c = criterion; s.md = max_depth if max_depth is not None else 10**9
s.msl = min_samples_leaf; s.task = task
def imp(s, Y): # 分類: クラス別件数 (..., K) / 回帰: (合計, 2乗の合計, 件数)
if s.task == "reg":
sm, sq, n = Y
return sq / n - (sm / n) ** 2 # 分散 = 二乗誤差の平均
p = Y / Y.sum(-1, keepdims=True)
if s.c == "gini":
return 1 - (p ** 2).sum(-1)
with np.errstate(divide="ignore", invalid="ignore"):
return -np.where(p > 0, p * np.log2(p), 0).sum(-1)
def best(s, X, y):
n = len(y); best = (np.inf, None, None)
for f in range(X.shape[1]):
o = np.argsort(X[:, f], kind="stable"); xs, ys = X[o, f], y[o]
k = np.arange(1, n) # 左に k 件
ok = (xs[1:] > xs[:-1] + 1e-7) & (k >= s.msl) & (n - k >= s.msl)
if not ok.any():
continue
if s.task == "reg":
cs, cq = np.cumsum(ys)[:-1], np.cumsum(ys ** 2)[:-1]
il = s.imp((cs, cq, k))
ir = s.imp((ys.sum() - cs, (ys ** 2).sum() - cq, n - k))
else:
oh = np.eye(s.K)[ys]; cl = np.cumsum(oh, 0)[:-1]; cr = oh.sum(0) - cl
il, ir = s.imp(cl), s.imp(cr)
w = np.where(ok, (k * il + (n - k) * ir) / n, np.inf)
i = np.argmin(w)
if w[i] < best[0] - 1e-12: # 同点なら先に見た列を残す
best = (w[i], f, xs[i] / 2 + xs[i + 1] / 2)
return best
def grow(s, X, y, d):
node = {"v": y.mean() if s.task == "reg" else np.bincount(y, minlength=s.K)}
if d >= s.md or len(y) < 2 * s.msl:
return node
parent = s.imp((y.sum(), (y**2).sum(), len(y))) if s.task == "reg" \
else s.imp(np.bincount(y, minlength=s.K)[None])[0]
w, f, t = s.best(X, y)
if f is None or parent <= 1e-12 or w >= parent - 1e-12:
return node
m = X[:, f] <= t
node.update(f=f, t=t, L=s.grow(X[m], y[m], d + 1), R=s.grow(X[~m], y[~m], d + 1))
return node
def fit(s, X, y):
X = np.asarray(X, np.float32).astype(np.float64) # scikit-learn と同じく float32 経由
if s.task == "clf":
s.cl, y = np.unique(y, return_inverse=True); s.K = len(s.cl)
s.root = s.grow(X, y, 0); return s
def predict(s, X):
X = np.asarray(X, np.float32).astype(np.float64); out = []
for x in X:
nd = s.root
while "f" in nd:
nd = nd["L"] if x[nd["f"]] <= nd["t"] else nd["R"]
out.append(nd["v"])
return np.array(out) if s.task == "reg" else s.cl[np.argmax(out, 1)]
4つのデータを7:3(層化)に分け、自作と scikit-learn を比べました。「構造が一致」は、random_state 0〜19 のどれかで、すべての分割(列としきい値)が自作と同じ木になったかどうかです。
| データ・基準・深さ | 葉の数(自作/sk) | テスト正解率(自作/sk) | 構造が一致 | 予測の一致率 |
|---|---|---|---|---|
| iris・gini・3 | 5/5 | 0.9556/0.9556 | 一致(rs=7) | 100% |
| wine・entropy・3 | 6/6 | 0.9444/0.9444 | 一致(rs=1) | 100% |
| breast_cancer・gini・3 | 8/8 | 0.9064/0.9064 | 一致(rs=1) | 100% |
| digits・gini・3 | 8/8 | 0.4759/0.4759 | 一致(rs=0) | 100% |
| wine・gini・3 | 8/8 | 0.8889/0.9630 | 20回では出ず | 92.6% |
| breast_cancer・gini・無制限 | 16/16 | 0.9181/0.9064 | 20回では出ず | 98.8% |
| digits・gini・無制限 | 136/136 | 0.8519/0.8426 | 20回では出ず | 91.5% |
| diabetes・回帰・4 | 16/16 | 予測の差 最大 0.0 | 一致(rs=0) | 完全一致 |
深い木で形が一致しないのは、同点の選び方の違いです。それを確かめるため、scikit-learn の木(random_state=0・深さ無制限)のすべての分割ノードについて、そのノードに来た訓練データで自作の全候補を計算し、scikit-learn が選んだ分割が「最良の値」と一致するかを調べました。
| データ・基準 | 分割ノード数 | 最良と同点の候補が複数あったノード | skの分割が最良でなかったノード |
|---|---|---|---|
| iris・gini | 7 | 3 | 0 |
| iris・entropy | 9 | 5 | 0 |
| wine・gini | 8 | 4 | 0 |
| breast_cancer・gini | 15 | 8 | 0 |
| breast_cancer・entropy | 11 | 4 | 0 |
| digits・gini | 135 | 80 | 0 |
| digits・entropy | 109 | 64 | 0 |
scikit-learn が選んだ分割は、全ノードで自作の計算による最良の値と一致しました。違いは同点の中からどれを選ぶかだけで、その選び方の差が、深い木ではテストの正解率で1〜7点の差になって表れます。ジニ不純度で分割を選ぶ、という理解で scikit-learn の木を読み解けることが確かめられました。
速さは、digits(1,257件・64列)の深さ無制限で自作 0.92秒、scikit-learn 0.014秒でした。自作は仕組みの確認用で、業務では scikit-learn(Cython 実装)を使います。numpy の配列操作に慣れていない場合は、関連記事「NumPyの使い方 基本一覧|配列の作成・演算・形状変更」が参考になります。
決定木の深さと過学習(訓練・テストの精度)
この節では、決定木を深くすると訓練とテストの精度がどう開くかを、実測の表で分かります。
過学習とは、訓練データに合わせすぎて、新しいデータで当たらなくなることです。決定木は深さを制限しないと、葉が1種類だけになるまで分け続けるので、訓練データの正解率は必ず100%近くまで上がります。見るべきはテスト(学習に使っていないデータ)の正解率です。
乳がんデータ(breast_cancer・569件・30列)
7:3 に分け、random_state 0〜9 の10回の平均です。CV は訓練データでの5分割交差検証の平均です。
| max_depth | 訓練 | テスト | 5分割CV | 葉の数 |
|---|---|---|---|---|
| 1 | 0.9322 | 0.8889 | 0.9047 | 2 |
| 2 | 0.9422 | 0.9064 | 0.9322 | 4 |
| 3 | 0.9784 | 0.9058 | 0.9247 | 8 |
| 4 | 0.9862 | 0.9105 | 0.9272 | 12 |
| 5 | 0.9975 | 0.9111 | 0.9297 | 16.8 |
| 無制限(実際は6) | 1.0000 | 0.9146 | 0.9247 | 17.5 |
訓練は深さ6で1.0に達しますが、テストは 0.89〜0.91 の間でほとんど動きません。このデータは「worst perimeter」など少数の列でほぼ分かれるため、深くしても得るものが少ない例です。
ノイズの多い2つの三日月(make_moons・600件・noise=0.35)
半分を訓練、半分をテストにしました。2つのクラスが重なり合うデータで、過学習がはっきり出ます。
| max_depth | 訓練 | テスト | 差 | 葉の数 |
|---|---|---|---|---|
| 1 | 0.7867 | 0.7833 | 0.003 | 2 |
| 2 | 0.9067 | 0.8500 | 0.057 | 4 |
| 4 | 0.9100 | 0.8467 | 0.063 | 12 |
| 6 | 0.9533 | 0.8533 | 0.100 | 20 |
| 8 | 0.9767 | 0.8567 | 0.120 | 29 |
| 無制限(実際は11) | 1.0000 | 0.8433 | 0.157 | 38 |
深さ2で葉4枚の木と、深さ11で葉38枚の木のテスト正解率はほぼ同じ(0.850 と 0.843)で、訓練との差は 0.057 から 0.157 に開きました。訓練とテストの差が開き続け、テストが伸びなくなったところが、それ以上深くしても意味がない合図です。文字で描くと次のようになります。
深さ 1 2 4 6 8 無制限
訓練 .79 .91 .91 .95 .98 1.00 ← 深くするほど上がり続ける
テスト .78 .85 .85 .85 .86 .84 ← 深さ2でほぼ頭打ち
決定木の剪定(max_depth・min_samples_leaf・ccp_alpha)
この節では、剪定の3つの方法と、それぞれで葉の数と精度がどう変わるかが分かります。
剪定には、育てる前に止める事前剪定と、育てたあとで枝を刈る事後剪定があります。
事前剪定(育てる途中で止める)
max_depth:深さの上限
min_samples_leaf:葉に残す最小の件数
min_samples_split・max_leaf_nodes・min_impurity_decrease
事後剪定(育ててから刈る)
ccp_alpha:コスト複雑度剪定
効果の小さい枝から順に刈る
cost_complexity_pruning_path で候補の値が出る
min_samples_leaf(葉の最小件数)
breast_cancer(random_state=0)で、葉に残す最小の件数を変えました。
| min_samples_leaf | 訓練 | テスト | 葉の数 | 深さ |
|---|---|---|---|---|
| 1(既定) | 1.0000 | 0.9064 | 16 | 6 |
| 5 | 0.9749 | 0.9123 | 11 | 4 |
| 10 | 0.9673 | 0.9123 | 8 | 4 |
| 20 | 0.9322 | 0.8889 | 6 | 3 |
| 50 | 0.9322 | 0.8889 | 4 | 2 |
葉を5〜10件以上にすると、1〜2件のために作られた枝が消え、テストが少し上がりました。20件以上では削りすぎです。min_samples_leaf は「葉の中の割合=確率」が極端にならないようにする効果もあります。
ccp_alpha(コスト複雑度剪定)
公式ドキュメントの定義は R_α(T) = R(T) + α × 葉の数 です。R(T) は葉の不純度を件数の割合で重み付けした合計で、α が大きいほど「葉が多いこと」への罰が重くなります。各内部ノード t について (R(t) − R(T_t)) ÷ (T_t の葉の数 − 1)(その枝を刈ったときの不純度の増え方を、減る葉1枚あたりにした値)を計算し、いちばん小さいノードから刈ります。
この式を自作で計算すると、breast_cancer の木での最小値は 0.0024917978…(ノード26)で、cost_complexity_pruning_path が返す最初の α と同じ値でした。α を順に指定した結果です。
| ccp_alpha | 葉の数 | 深さ | 訓練 | テスト |
|---|---|---|---|---|
| 0(既定) | 16 | 6 | 1.0000 | 0.9064 |
| 0.0025 | 12 | 5 | 0.9950 | 0.9123 |
| 0.00492 | 7 | 4 | 0.9824 | 0.9181 |
| 0.00879 | 6 | 4 | 0.9774 | 0.9181 |
| 0.0196 | 4 | 3 | 0.9623 | 0.9006 |
| 0.0266 | 2 | 1 | 0.9322 | 0.8889 |
| 0.3435 | 1 | 0 | 0.6281 | 0.6257 |
α の候補は12個出ました。最後の α では根だけになり、多い方のクラスを答えるだけ(0.63)です。α はテストデータで選ばず、訓練データの交差検証で選びます。
from sklearn.model_selection import GridSearchCV
path = DecisionTreeClassifier(random_state=0).cost_complexity_pruning_path(X_train, y_train)
grid = GridSearchCV(DecisionTreeClassifier(random_state=0),
{"ccp_alpha": path.ccp_alphas[:-1]}, cv=5).fit(X_train, y_train)
print(grid.best_params_, grid.best_score_, grid.score(X_test, y_test),
grid.best_estimator_.get_n_leaves())
{'ccp_alpha': np.float64(0.008793969849246231)} 0.9323 0.9181 6
交差検証で選ばれた α=0.0088 の木は葉6枚で、テストは 0.9181 でした。max_depth と min_samples_leaf を格子で探した場合(max_depth=5・min_samples_leaf=2)もテストは同じ 0.9181 でしたが、葉は14枚です。同じ精度なら葉の少ない方が読みやすく、説明に使えます。
剪定の値の選び方
テストで選ぶと、テストへの過学習になり、本番の精度を高く見積もる
回帰木:数値を予測する決定木(DecisionTreeRegressor)
この節では、数値を予測する回帰木の仕組みと、苦手なこと(外挿)が分かります。
回帰木は、不純度の代わりに二乗誤差(分散)が最も小さくなる分割を選び、葉では訓練データの平均を答えます。sin カーブに雑音を足した80点で、深さ2の木を作りました。
|--- x <= 3.35
| |--- x <= 0.43
| | |--- value: [0.16]
| |--- x > 0.43
| | |--- value: [0.68]
|--- x > 3.35
| |--- x <= 3.60
| | |--- value: [-0.23]
| |--- x > 3.60
| | |--- value: [-0.75]
予測は階段の形になります。x=0.5 でも 1.5 でも 3.0 でも答えは 0.6795 で、本当の値(0.48・1.00・0.14)とは関係なく同じです。深さを変えたときの誤差です(テストは雑音のない本当の sin カーブ200点との差)。
| max_depth | 葉の数 | 訓練のMSE | 本当の曲線とのMSE |
|---|---|---|---|
| 1 | 2 | 0.0942 | 0.0913 |
| 3 | 8 | 0.0346 | 0.0321 |
| 5 | 25 | 0.0125 | 0.0282 |
| 無制限 | 80 | 0.0000 | 0.0425 |
無制限では葉が80枚(1点に1枚)になり、雑音まで覚えて誤差が増えました。
範囲の外は予測できない(外挿)
y = 3x + 2 の x=0〜9 で学習し、x=20 を予測させました。
決定木(回帰) x=20 → 29.0 ← 訓練の最大 x=9 のときの値のまま
線形回帰 x=20 → 62.0 ← 式どおり
公式ドキュメントも「決定木の予測はなめらかでも連続でもなく区分的に一定なので、外挿が苦手」と書いています。売上の将来予測のように、訓練データにない大きさの値を答えさせる用途には向きません。
糖尿病データ(load_diabetes)での回帰木
| 設定 | 葉の数 | 訓練R² | テストR² |
|---|---|---|---|
| max_depth=2 | 4 | 0.4868 | 0.2102 |
| max_depth=4 | 16 | 0.6578 | 0.1386 |
| max_depth 無制限 | 298 | 1.0000 | −0.1332 |
| min_samples_leaf=40 | 6 | ― | 0.2089 |
| (参考)線形回帰 | ― | ― | 0.3929 |
無制限の木はテストの R² がマイナス(平均を答えるより悪い)になりました。このデータは目的変数が各列とほぼ直線的な関係なので、1本の決定木より線形回帰の方が当たります。回帰木の分割基準は squared_error(既定)・absolute_error・poisson で、scikit-learn 1.9.1 では friedman_mse を指定すると「1.11 で削除予定、squared_error と同じ」という FutureWarning が出ました(11節)。直線の当てはめ方そのものは、関連記事「最小二乗法とは|公式・手計算の例とPythonでの求め方を実例で解説」で扱っています。
決定木で特徴量のスケール(標準化)が要らない理由
この節では、決定木に標準化・正規化が要らない理由と、それ以外に要る前処理が分かります。
決定木の分割は「値の小さい順に並べて、どこで区切るか」だけで決まります。値を何倍しても、平均を引いても、対数をとっても、並び順が変わらなければ同じ区切りが選ばれます。breast_cancer で確かめた結果です。予測の一致は、テスト171件の予測がすべて同じだったかどうかです。
| 前処理 | 決定木のテスト | 決定木の葉 | 決定木の予測 | k近傍法のテスト |
|---|---|---|---|---|
| そのまま | 0.9064 | 16 | 基準 | 0.9123 |
| StandardScaler | 0.9064 | 16 | 全件同じ | 0.9474 |
| MinMaxScaler | 0.9064 | 16 | 全件同じ | 0.9532 |
| 全列を1000倍 | 0.9064 | 16 | 全件同じ | 0.9123 |
| log1p(対数) | 0.9064 | ― | ― | ― |
距離を使う k 近傍法はスケーリングで 0.912 から 0.953 に変わりますが、決定木は1件も変わりませんでした。
決定木でも要る前処理
- 文字の列は数値にする:scikit-learn の決定木はカテゴリ変数をそのまま扱えないと公式ドキュメントにあります。「晴れ」をそのまま渡すと
could not convert string to float: '晴れ'で止まりました。決定木なら OrdinalEncoder(カテゴリに番号を振る)で動きます。 - 欠損値:scikit-learn 1.9.1 では
splitter="best"(既定)で NaN のまま学習でき、手元でも NaN を含む6行で学習と予測が通りました。学習時に欠損があった列は、欠損を左右どちらに送るのが良いかも一緒に選ばれます。学習時に欠損が無かった列で予測時に欠損が来た場合は、件数の多い子ノードへ送られます(公式ドキュメント)。 - クラスの偏り:あるクラスが大半を占めると木が偏る、と公式ドキュメントは注意しています。
class_weight="balanced"か、データの調整を検討します。
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OrdinalEncoder
df = pd.DataFrame({"天気": ["晴れ", "雨", "曇り", "晴れ", "雨", "曇り", "晴れ", "雨"],
"気温": [25, 18, 22, 30, 15, 20, 27, 19],
"外出": [1, 0, 1, 1, 0, 0, 1, 0]})
ct = ColumnTransformer([("cat", OrdinalEncoder(), ["天気"])], remainder="passthrough")
model = make_pipeline(ct, DecisionTreeClassifier(random_state=0))
model.fit(df[["天気", "気温"]], df["外出"])
print(model.predict(pd.DataFrame({"天気": ["曇り"], "気温": [21]}))) # [0]
このデータでは「気温 ≤ 21.0」の1回の分割だけで分かれ、天気の列は使われませんでした。表データの読み込みや列の変換は、関連記事「pandasの使い方 逆引き81選|pandas 3.0の実行結果つき」で扱っています。
決定木の特徴量重要度の読み方と偏り
この節では、feature_importances_ の意味と、意味のない列が上位に来る実例が分かります。
feature_importances_ は、その列での分割が木全体の不純度をどれだけ減らしたかの合計(全列で1になるよう正規化)です。訓練データで計算されるため、過学習した木では「たまたま訓練データを分けられた列」も高く出ます。
breast_cancer の30列に、意味のない2列(0〜568の乱数の並べ替え=ID のような列と、0か1の乱数=コイン)を足して、深さ無制限の木を学習しました。
| 順位 | 不純度ベース(feature_importances_) | 値 | permutation(テストで20回) | 値 |
|---|---|---|---|---|
| 1 | worst perimeter | 0.7461 | worst concave points | 0.1158 |
| 2 | worst concave points | 0.0988 | area error | 0.1108 |
| 3 | worst radius | 0.0410 | worst perimeter | 0.0863 |
| 4 | worst texture | 0.0224 | radius error | 0.0518 |
| 5 | 乱数ID | 0.0191 | worst smoothness | 0.0240 |
| ― | 乱数コイン | 0.0000 | 乱数ID | −0.0006(±0.008) |
乱数の ID 列が32列中5位に入りました。値が569通りある列は区切り位置の候補が多く、深い枝で少数の訓練データを分けるのに使われやすいためです。0か1しかないコインは使われませんでした。テストデータで列の値をかき混ぜて精度の落ち方を見る permutation importance では、乱数 ID は −0.0006 で、ばらつきの範囲内のゼロでした。scikit-learn の公式ドキュメントも、不純度ベースの重要度は値の種類が多い列に強く偏り、過学習しているときは未知のデータで役に立たない列を重要と示しうる、と書いています。
1不純度ベースは「木の作り」
どの列で分けたかを表す。速いが訓練データで計算され、値の種類が多い列が有利。
2permutation は「予測への効き」
テストデータで列を壊したときの精度の落ち方。時間はかかるが、意味のない列はほぼ0になる。
3説明に使うなら両方を見る
上位が食い違う列は、相関の強い列が他にあるか、過学習で選ばれた列かを疑う。
学習と予測にかかる時間も測りました(make_classification・20列・深さ無制限)。公式ドキュメントでは、学習の計算量はおよそ 列数 × 件数 × log(件数)、予測は深さに比例するとされています。
| 件数 | 学習 | 全件の予測 | 葉の数 | 深さ |
|---|---|---|---|---|
| 1,000 | 0.025秒 | 0.0003秒 | 35 | 9 |
| 10,000 | 0.538秒 | 0.0017秒 | 616 | 30 |
| 50,000 | 4.945秒 | 0.0343秒 | 2,833 | 39 |
決定木のよくあるエラー・つまずき
この節では、決定木を scikit-learn で動かしたときに出たエラーの原文と直し方が分かります。すべて scikit-learn 1.9.1 で出た文面です。
| 症状・エラー原文 | 原因 | 直し方 |
|---|---|---|
ValueError: could not convert string to float: '晴れ' | 文字の列をそのまま渡した | OrdinalEncoder・OneHotEncoder で数値にする |
ValueError: Unknown label type: continuous. Maybe you are trying to fit a classifier, which expects discrete classes on a regression target with continuous values. | 数値を当てたいのに Classifier を使った | DecisionTreeRegressor にする |
ValueError: Expected 2D array, got 1D array instead: | 1列の X を1次元で渡した | X.reshape(-1, 1) か df[["列"]] |
ValueError: X has 1 features, but DecisionTreeClassifier is expecting 2 features as input. | 予測時の列数が学習時と違う | 学習と同じ列を同じ順で渡す |
NotFittedError: This DecisionTreeClassifier instance is not fitted yet. | fit の前に predict した | 先に fit する |
InvalidParameterError: The 'max_depth' parameter of DecisionTreeClassifier must be an int in the range [1, inf) or None. Got 0 instead. | 深さ0は指定できない | 1以上か None |
InvalidParameterError: The 'criterion' parameter of DecisionTreeRegressor must be a str among {'poisson', 'absolute_error', 'squared_error'}. Got 'mse' instead. | 以前の版の名前 mse を使った | "squared_error" |
FutureWarning: Value "friedman_mse" for criterion is deprecated and will be removed in 1.11. | 1.9 で非推奨 | "squared_error"(同じ結果) |
| 実行のたびに木と精度が変わる | 同点の分割をランダムに選んでいる | random_state=0 などで固定 |
| 訓練は1.0なのにテストが低い | 過学習 | max_depth・min_samples_leaf・ccp_alpha |
ccp_alpha に負の値を入れた場合も The 'ccp_alpha' parameter of DecisionTreeClassifier must be a float in the range [0.0, inf). Got -0.1 instead. で止まりました。引数の範囲は fit を呼んだときに確かめられます。
決定木を使うか決める判断表(ランダムフォレスト・XGBoostとの違い)
この節では、1本の決定木を使う場面と、ランダムフォレストや勾配ブースティング、線形モデルに切り替える場面が分かります。
| 状況 | 向いている手法 | 理由(この記事の実測・公式の記述) |
|---|---|---|
| ルールを人に説明したい・審査の根拠を残したい | 浅い決定木(葉10枚程度まで) | export_text でそのまま if 文になる |
| とにかく精度が要る表データ | 勾配ブースティング(XGBoost・LightGBM・HistGradientBoosting) | 1本の木は不安定で、深くしてもテストが伸びにくい |
| 手早く安定した精度がほしい | ランダムフォレスト | 多数の木の平均で、同点や小さなデータの違いによるぶれを減らす |
| 目的変数が列とほぼ直線的な関係 | 線形回帰・ロジスティック回帰 | diabetes で線形回帰 R² 0.39、回帰木は最良でも 0.21 |
| 訓練データの範囲の外を予測する | 線形モデルなど | 回帰木は x=20 でも x=9 の値(29)を答えた |
| 特徴量のスケールがばらばら・欠損がある | 決定木系 | スケーリングで予測が1件も変わらず、NaN も学習できる |
| 画像・音声・文章 | ニューラルネットワーク | 画素1つずつの「≤ しきい値」の分岐では形をとらえにくい |
| XOR のような列の組み合わせで決まる | 深めの木・アンサンブル・他の手法 | 公式ドキュメントが「決定木が表しにくい概念」として XOR・パリティを挙げる |
1本の決定木は、ランダムフォレスト・勾配ブースティングの部品です。決定木の分割の選び方と過学習の出方が分かると、それらのハイパーパラメータ(max_depth・min_samples_leaf など)が何を制限しているかが読めるようになります。
1まず浅い木で全体を見る
max_depth=3 で export_text を読み、どの列で分かれるかを確かめる。データの誤りに気づくことも多い。
2剪定は交差検証で決める
ccp_alpha の候補を cost_complexity_pruning_path で出し、GridSearchCV で選ぶ。
3精度が足りなければ束ねる
同じデータでランダムフォレスト・勾配ブースティングと比べ、説明しやすさと精度の釣り合いで選ぶ。
決定木のよくある質問
この節では、決定木について検索でよく調べられる疑問への答えが分かります。
Q. 決定木は教師あり学習ですか。
はい。正解のラベル(分類)や値(回帰)が付いたデータから、分岐のルールを学ぶ教師あり学習です。
Q. ジニ不純度とジニ係数は同じものですか。
別のものです。ジニ不純度は「1 − 各クラスの割合の2乗の合計」で、ノードの混ざり具合を表します。ジニ係数は所得の不平等などに使う指標で、ローレンツ曲線から計算します。名前は同じ統計学者に由来します。
Q. ジニ不純度の最大値はいくつですか。
クラスが K 個で同じ割合のとき 1 − 1/K です。2クラスなら0.5、3クラスなら0.667です。エントロピーの最大値は log₂K(2クラスで1.0)です。
Q. 決定木で3つ以上に分かれる分岐は作れますか。
scikit-learn(CART)は常に2つに分けます。ID3・C4.5 のように、カテゴリの数だけ枝を分けるアルゴリズムもあります。2分岐でも、同じ列を何度も使えば3つ以上の区間に分けられます。
Q. しきい値が 4.95 のような半端な値になるのはなぜですか。
隣り合う2つの値(4.9 と 5.0)の中間を区切りにするためです。手計算の例では、契約月数 5 と 9 の間で分けて 7.0 になりました。
Q. 決定木の図を出すにはどうしますか。sklearn.tree.plot_tree(matplotlib を使う)か、export_graphviz で DOT 形式の文字列を出して Graphviz で描きます。文字だけでよければ export_text が最も手軽です。
Q. 決定木の深さはいくつにすればよいですか。
データによります。この記事の例では、make_moons は深さ2、breast_cancer は葉6〜7枚(深さ4)でテストが頭打ちになりました。決め打ちせず、交差検証で max_depth か ccp_alpha を選びます。
Q. 決定木に標準化は要りますか。
要りません。値の並び順だけで分けるので、StandardScaler・MinMaxScaler・1000倍のどれでも予測は1件も変わりませんでした。同じ Pipeline で k 近傍法や SVM と比べる場合は、それらのためにスケーリングを入れます。
Q. predict_proba が 1.0 や 0.0 ばかりになります。
葉の中の割合を返すためで、葉が純粋(1種類だけ)になるまで育てた木では 1.0 か 0.0 になります。確率として使うなら min_samples_leaf を大きくするか、CalibratedClassifierCV で補正します。
Q. 決定木とランダムフォレストの違いは何ですか。
ランダムフォレストは、データの一部と列の一部をランダムに選んで多数の決定木を作り、その多数決(回帰は平均)で答えます。1本の木の不安定さ(この記事の wine の例で正解率が7通りに分かれたこと)を平均でならす仕組みです。
Q. 決定木の結果を Excel で使えますか。
export_text の出力は if 文の形なので、浅い木なら IF 関数の入れ子に書き直せます。深い木は手で写すと誤りやすいので、Python から予測結果を書き出す方が確実です。
決定木のまとめ
この節では、記事の要点を5つで振り返ります。
- 決定木とは、「特徴量 ≤ しきい値か」の質問でデータを2つに分け続け、葉の多数決(回帰は平均)で予測する手法。経営の「意思決定の決定木」とは別物。
- ジニ不純度は
1 − Σp²。分割後の重み付きの不純度が最小になる分割が選ばれる。手計算・numpy の自作・scikit-learn で値が一致した。 - 同点の分割はランダムに選ばれ、wine では同じ設定でテストの正解率が 0.852〜0.963 に分かれた。random_state を固定し、複数回で比べる。
- 過学習:深さ無制限の木は訓練1.0になるがテストは伸びない。剪定は max_depth・min_samples_leaf か ccp_alpha で、交差検証で選ぶ(葉16→6枚でテスト 0.906→0.918)。
- スケーリングは要らないが、文字の列の数値化、外挿が苦手なこと、不純度ベースの重要度の偏り(乱数 ID が5位)には注意する。
参考・出典
この節では、この記事で確かめた公式の資料が分かります(いずれも確認日 2026年11月5日)。
- scikit-learn 1.9.1 documentation「1.10. Decision Trees」(CART・欠損値・長所と短所・コスト複雑度剪定・計算量):https://scikit-learn.org/stable/modules/tree.html
- scikit-learn 1.9.1「DecisionTreeClassifier」(criterion・random_state の説明):https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html
- scikit-learn 1.9.1「DecisionTreeRegressor」:https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeRegressor.html
- scikit-learn 1.9.1「5.2. Permutation feature importance」(不純度ベースの重要度との関係):https://scikit-learn.org/stable/modules/permutation_importance.html
- scikit-learn「Post pruning decision trees with cost complexity pruning」(例):https://scikit-learn.org/stable/auto_examples/tree/plot_cost_complexity_pruning.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「決定木とは|ジニ不純度・剪定・過学習をPythonで自作して実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。