決定木は、「この値以下なら左、それ以外なら右」という条件で行を分け、最後に到着した葉で予測するモデルです。分岐を図で読めるため、どの条件で予測が変わるかを確かめやすくなります。ただし、分岐が読めることと、その条件を変えれば結果が改善することは別です。
ここでは、8行の小さな例で木の仕組みを確かめ、別の合成データで剪定と評価の分離を実行します。購入の例はすべて教材で、実店舗や顧客の成果ではありません。
30秒で要点
- 葉までの条件と一緒に、学習件数、クラス構成、分岐の深さを読む。
- 深さ・最小葉・剪定の設定は、学習用の当てはまりだけで選ばない。
- 学習、設定の選択、最後の評価には別の役割がある。最終評価を見て選び直すと、その値は最終確認ではなくなる。
- 少数クラスでは正解率だけに頼らず、対象割合、見逃し、誤検知、順位付けを確認する。
分岐が小さくする「混ざり具合」とは
分類木は、例えば購入あり・なしのラベルができるだけ分かれるように、列と閾値を選びます。Gini不純度はクラス割合をpとすると 1 - Σp² です。半分ずつなら0.5、一方だけなら0になります。別の指標であるエントロピーは -Σp log(p) で、同じく一方だけの葉では0です。
分割後は、左右の不純度を行数、重みがあれば重みの合計で加重して比較します。例えば仮想の8行で正例6・負例2ならGiniは0.375。二つの純粋な葉へ分かれる分割では、その加重不純度は0です。これは学習データの当てはまりであり、新しい行の正解率ではありません。
回帰木は数値を予測します。二乗誤差を基準にする場合、葉の予測は学習目的値の平均です。絶対誤差なら中央値が対応するなど、基準によって意味が変わります。scikit-learnの決定木ガイドに分類・回帰の式と限界が整理されています。
決定木は各段階で分割を選ぶ貪欲な手法です。選んだ木が全体で唯一の最適解とは限らず、近い候補があると小さなデータ変化で分岐が変わります。浅い木は読みやすくても、単純にするほど常に予測が良くなるわけではありません。
8行の例:木が伸びない理由を調べる
以下は年齢と支出の合成入力です。2026年10月4日、Python 3.12.10、NumPy 2.1.3、pandas 2.2.3、scikit-learn 1.5.2、Matplotlib 3.9.2で実行しました。固定した再現環境であり、最新版を推奨する記述ではありません。図はPNGへ保存するため、画面を持たない環境でも実行できます。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.metrics import accuracy_score
X = pd.DataFrame({"age": [22, 25, 47, 52, 46, 56, 55, 60],
"spend": [200, 220, 500, 520, 480, 600, 590, 610]})
y = np.array([0, 0, 1, 1, 1, 1, 1, 1])
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=.3, random_state=42)
clf = DecisionTreeClassifier(max_depth=3, random_state=42).fit(X_tr, y_tr)
print("Train/test rows:", len(y_tr), len(y_te))
print("Training classes:", clf.classes_)
print("Leaves:", clf.get_n_leaves())
print("Test accuracy:", round(accuracy_score(y_te, clf.predict(X_te)), 3))
names = {0: "no", 1: "yes"}
plot_tree(clf, feature_names=list(X.columns),
class_names=[names[int(c)] for c in clf.classes_], filled=True)
plt.savefig("tree-small-split.png", dpi=150)
plt.close()
# The second split is a separate diagnostic of the original pruning setup.
X6, X2, y6, y2 = train_test_split(X, y, test_size=.2, random_state=42)
stump = DecisionTreeClassifier(min_samples_leaf=20, class_weight="balanced",
random_state=42).fit(X6, y6)
print("Other train/test rows:", len(y6), len(y2))
print("min_leaf=20 leaves:", stump.get_n_leaves())
# A full-data fit illustrates a split, but is not held-out assessment.
illustration = DecisionTreeClassifier(max_depth=2, random_state=42).fit(X, y)
print("Full-data illustration leaves:", illustration.get_n_leaves())
plot_tree(illustration, feature_names=list(X.columns),
class_names=[names[int(c)] for c in illustration.classes_], filled=True)
plt.savefig("tree-full-data-illustration.png", dpi=150)
plt.close()
最初の分割は学習5件、評価3件です。この乱数設定では学習側にラベル1しか残らず、木は1枚の葉になります。評価正解率は1/3、約0.333でした。学習したクラス順をclasses_から読んで表示名を作るのは、一方のクラスしかないときに「no」と誤表示しないためです。
次の分割は学習6件、評価2件です。min_samples_leaf=20は、分割後の左右の子にそれぞれ20件以上必要という条件なので、6件の根から分岐できません。根に20件必要という意味ではなく、条件を満たさない場合はそのまま葉になります。小さいデータに大きい葉数制約を置くと、剪定の設定を比較しても同じ予測しか得られないことがあります。最小葉とクラス順のAPI
最後は8行すべてで学習し、分岐を観察するためだけの木です。葉は2枚になりますが、この同じ8行に対する当てはまりを、未知の顧客での性能とは呼びません。小さな例でfit、predict、図の読み方を覚えることと、業務の採用判断に使う評価を分けます。
木の成長を止める方法と、後から剪定する方法
表が収まらない場合は、横にスクロールできます
| 設定 | 変えるもの | 注意点 |
|---|---|---|
max_depth | 根から葉までの最大の深さ | 浅すぎれば必要な条件分岐も表せない |
min_samples_leaf | 両方の子に残す最小件数 | データ規模と対応させる。大きすぎると根だけになる |
min_impurity_decrease | 分割に必要な不純度の減少 | 重みを使う場合はその定義も確認する |
ccp_alpha | 葉を増やすことへの罰則 | 候補値の作成と選択に最終評価を使わない |
剪定は、細かくなった分岐を取り除くことです。cost-complexity方式では、おおまかに 葉の不純度の合計 + α × 葉数 を考えます。ここで不純度の合計は、各葉の学習重みを全体の重みで割って加重した値です。αを大きくすると、葉を増やすことへの負担が大きくなります。
cost_complexity_pruning_pathが返すimpuritiesは、その不純度の合計です。葉の枚数ではありません。葉数は各候補を学習してget_n_leaves()で求めます。公式の剪定例も、不純度とノード数・深さを別に図示しています。
パスを作る木と、αごとに学習する木で最小葉や深さの条件が違うと、同じ候補系列を比較しているつもりでも意味がずれます。次の例では事前制約とクラス重みを同じ辞書で渡します。
設定選びに使った評価を、最後の成績にしない
学習用データは木を作るため、検証用データはαなどの設定を選ぶため、テスト用データは選び終わった手順を最後に評価するために分けます。検証で高い点になった設定を選べば、その検証値は選択の影響を受けます。公式のモデル選択の説明
次の例は合成1200行を学習720・検証240・テスト240に分けます。候補19個のうち検証APが最大のものを選び、同点なら大きいαを採ります。AP(平均適合率)は、再現率が増えた分を重みにして適合率を足す順位評価です。台形で求めるPR曲線下面積と同じ値とは限らないため、単に「PR-AUC」と表記しません。average_precision_scoreの定義
選択後は再学習せず、その学習済みモデルをテストへ適用します。学習と検証を結合して再学習する運用もありますが、その場合は別のモデルになります。ここでは評価対象を明確にするため、処理を分けています。
独立して実行できる剪定・重みの例
このコードは前の8行を引き継ぎません。3列の連続値と確率的なラベルを新しく生成します。分割は、互いに独立な行を仮定した教材用です。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import average_precision_score, roc_auc_score, confusion_matrix
# Independent continuous synthetic data; no real purchase observations.
rng = np.random.default_rng(120)
X = rng.normal(size=(1200, 3))
logit = -1.8 + 1.3 * (X[:, 0] > .2) + .8 * X[:, 1] - .5 * X[:, 2]
y = (rng.random(1200) < 1 / (1 + np.exp(-logit))).astype(int)
X_dev, X_test, y_dev, y_test = train_test_split(
X, y, test_size=.2, random_state=10, stratify=y)
X_train, X_valid, y_train, y_valid = train_test_split(
X_dev, y_dev, test_size=.25, random_state=11, stratify=y_dev)
settings = dict(max_depth=6, min_samples_leaf=20,
class_weight="balanced", random_state=42)
path = DecisionTreeClassifier(**settings).cost_complexity_pruning_path(X_train, y_train)
models = [DecisionTreeClassifier(ccp_alpha=float(a), **settings).fit(X_train, y_train)
for a in path.ccp_alphas]
def positive_score(model, values):
column = np.flatnonzero(model.classes_ == 1)
if len(column) != 1:
raise ValueError("Training data needs the positive class")
return model.predict_proba(values)[:, column[0]]
valid_ap = [average_precision_score(y_valid, positive_score(m, X_valid)) for m in models]
best = max(range(len(models)), key=lambda i: (valid_ap[i], path.ccp_alphas[i]))
selected = models[best] # No refit: final test evaluates this fixed trained model.
test_score = positive_score(selected, X_test)
leaves = np.array([m.get_n_leaves() for m in models])
print("Train/valid/test:", len(y_train), len(y_valid), len(y_test))
print("Positive counts:", y_train.sum(), y_valid.sum(), y_test.sum())
print("Candidates:", len(models))
print("Selected alpha:", round(float(path.ccp_alphas[best]), 6))
print("Selected leaves:", selected.get_n_leaves())
print("Validation AP:", round(valid_ap[best], 3))
print("Final test AP:", round(average_precision_score(y_test, test_score), 3))
print("Final test ROC-AUC:", round(roc_auc_score(y_test, test_score), 3))
print("Final test confusion at 0.5:\n", confusion_matrix(y_test, test_score >= .5, labels=[0, 1]))
fig, axes = plt.subplots(2, 1, figsize=(6, 6), sharex=True)
axes[0].step(path.ccp_alphas, path.impurities, where="post", marker="o")
axes[0].set_ylabel("Weighted total leaf impurity")
axes[1].step(path.ccp_alphas, leaves, where="post", marker="o")
axes[1].set_ylabel("Number of leaves")
axes[1].set_xlabel("ccp_alpha (same training data)")
fig.tight_layout()
fig.savefig("tree-pruning.png", dpi=150)
plt.close(fig)
固定環境では、学習・検証・テストの正例は199・67・67件でした。選択したαは約0.000708、葉数は20、検証APは0.420。最後のテストではAPが0.487、ROC-AUCが0.727でした。これらはこの合成例の出力で、決定木の標準的な性能ではありません。乱数設定を変えた安定性の検証も、この一回の結果からは言えません。
テストの0.5しきい値では、真陰性129・偽陽性44・偽陰性25・真陽性42です。正例67件中42件を拾い、陽性と予測した86件中42件が正例でした。つまり再現率は約62.7%、適合率は約48.8%です。施策の実行費用と見逃しの費用が分からなければ、この値から運用可否は決まりません。
不均衡なデータで、何を改善したいのか
正例が5%の仮想データなら、すべて負例と答えても正解率は95%です。しかし正例は一件も拾えていません。正解率だけでなく、見逃した数、誤検知した数、順位を付けた場合に何件まで対応できるかを確認します。
class_weight="balanced"は、学習データのクラス頻度に反比例する重みを付けます。実際の損失額を自動計算する機能ではありません。また、predict_probaは葉にいるクラスの割合を返し、重みを付ければその重みが反映されます。0.8と出たから、同じ条件の顧客の80%が購入すると確認できたわけではありません。
しきい値や校正を選ぶ場合も、その作業用の検証を用意します。最終テストを見ながらしきい値を動かし、その最良値を初見の性能として報告しないでください。担当者の対応可能件数、誤連絡の負担、取りこぼしの費用を一緒に記録します。
業務に移すときは、列を使える時点を先に決める
仮想のEC業務で翌週の購入を予測するなら、予測時点で分かる閲覧回数や過去の支出を用意し、翌週にしか分からない購入後の情報を説明変数へ混ぜません。欠測補完やカテゴリの符号化を使う場合も、その推定は学習側だけで行います。前処理と情報漏洩の公式説明
決定木は数値の尺度を揃えなくても分割できますが、欠測やカテゴリの意味を自動で理解するわけではありません。scikit-learnの木へ文字列カテゴリをそのまま渡すことと、一般論として決定木がカテゴリを扱えることは別です。補完・符号化をPipelineに入れれば分割内で学習しやすくなりますが、未来の情報が混ざった列自体を正しくする機能ではありません。
同じ顧客の複数行があるなら、顧客をまたぐ混入を避ける分割を検討します。未来の期間に使うなら、時間順の評価が必要になります。層別分割で正例割合を揃えても、この依存や時系列の違いは解決しません。
図の最上位が「在庫あり」でも、在庫を増やす介入効果が推定されたわけではありません。在庫情報をいつ記録したか、欠品中の閲覧がどう残ったか、ほかの条件の代理になっていないかを確かめます。木の分岐は調べる手掛かりとして使い、原因の結論にしません。
最小検証:採用する前に一枚の記録を作る
まず「誰について、いつの情報で、何日先を予測するか」を一文にします。次に、データの分割単位、候補の範囲、選択指標、最終評価の期間、しきい値、対応可能件数を記録してください。
学習では高い正解率でも検証で下がるなら、深さ・葉サイズ・剪定、不要な列、分割単位を見直します。複数の設定を試すほど選択の影響も増えるので、テストを使わず選択を完了させる手順を残します。標本が少ない場合は、業務の独立単位に合う交差検証を検討します。
採用後の見直しは、一律の週次再学習ではなく、ラベルが確定する遅れ、予測対象の変化、葉ごとの件数や誤り、担当者の処理量に合わせます。更新前後を同じ定義で比較し、評価が悪化したときに以前の手順へ戻せる状態にします。
練習するなら、合成例の最小葉を変えたときの葉数と検証APを比べてください。次に正例割合を変え、正解率だけでは見落とす数を計算します。ただし、その練習でテストを何度も見たなら、新たな最終評価として使い直せません。
木のばらつきを抑える候補はランダムフォレスト、数値予測の別の基準は線形回帰、問いから選び直す場合はデータサイエンス手法の一覧で比較できます。どの手法も、同じ評価条件で確かめる必要があります。