Lompat ke konten
nalar

Decision Tree Induction (Pohon Keputusan)

Konsep klasifikasi terarah, entropy, information gain (ID3), gain ratio (C4.5), Gini impurity (CART), split atribut numerik, overfitting dan pruning.

Notebook lab (pratinjau)

Unduh lalu jalankan di Jupyter, VS Code, atau Google Colab.

Unduh lab.ipynb

Lab Minggu 6 — Decision Tree Induction

Tujuan lab:

  1. Menghitung entropy, information gain, gain ratio, dan Gini pada dataset Play Tennis.
  2. Melatih pohon keputusan scikit-learn dan memvisualisasikan percabangannya.
  3. Melatih pohon pada data churn pelanggan, mengamati overfitting, dan melakukan pruning.
[1]
import sys
sys.path.insert(0, "..")

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from common.datasets import play_tennis, churn_pelanggan

df = play_tennis()
df

1. Entropy & Gini

[2]
def entropy(y):
    p = pd.Series(y).value_counts(normalize=True)
    return -(p * np.log2(p)).sum()

def gini(y):
    p = pd.Series(y).value_counts(normalize=True)
    return 1 - (p ** 2).sum()

print(f"Entropy(play) = {entropy(df.play):.3f}")
print(f"Gini(play)    = {gini(df.play):.3f}")
[3]
p = np.linspace(0.001, 0.999, 200)
plt.plot(p, -(p * np.log2(p) + (1 - p) * np.log2(1 - p)), label="entropy")
plt.plot(p, 1 - p**2 - (1 - p)**2, label="gini")
plt.xlabel("proporsi kelas positif"); plt.legend(); plt.title("Ukuran impurity (2 kelas)")
plt.show()

2. Information gain, gain ratio, gini split untuk tiap atribut

[4]
def info_gain(d, a, target="play"):
    n = len(d)
    return entropy(d[target]) - sum(len(g) / n * entropy(g[target]) for _, g in d.groupby(a))

def gain_ratio(d, a, target="play"):
    return info_gain(d, a, target) / entropy(d[a])

def gini_split(d, a, target="play"):
    n = len(d)
    return sum(len(g) / n * gini(g[target]) for _, g in d.groupby(a))

tabel = pd.DataFrame({a: {"gain": info_gain(df, a), "gain_ratio": gain_ratio(df, a),
                          "gini_split": gini_split(df, a)} for a in df.columns[:-1]}).T
tabel.sort_values("gain", ascending=False).round(3)

outlook terpilih sebagai root. Rincian per nilai:

[5]
pd.crosstab(df.outlook, df.play).assign(
    entropy=lambda t: [entropy(df[df.outlook == v].play) for v in t.index])

3. Rekursi: subset Sunny dan Rain

[6]
for v in ["Sunny", "Rain"]:
    sub = df[df.outlook == v].drop(columns="outlook")
    gains = {a: round(info_gain(sub, a), 3) for a in sub.columns[:-1]}
    print(v, "->", gains)

4. Decision tree scikit-learn pada Play Tennis

[7]
from sklearn.tree import DecisionTreeClassifier, export_text, plot_tree

X = pd.get_dummies(df.drop(columns="play"), dtype=int)
y = df["play"]
pohon = DecisionTreeClassifier(criterion="entropy", random_state=0).fit(X, y)
print(export_text(pohon, feature_names=list(X.columns)))
[8]
plt.figure(figsize=(12, 6))
plot_tree(pohon, feature_names=X.columns, class_names=pohon.classes_, filled=True, rounded=True)
plt.show()

Perhatikan: scikit-learn (CART) membuat split biner pada kolom one-hot, misalnya outlook_Overcast <= 0.5, berbeda dari ID3 yang bercabang multi.

5. Studi kasus: prediksi churn pelanggan

[9]
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

churn = churn_pelanggan()
Xc = pd.get_dummies(churn.drop(columns="churn"), dtype=int)
yc = churn["churn"]
X_tr, X_te, y_tr, y_te = train_test_split(Xc, yc, test_size=0.3, random_state=42, stratify=yc)
print(X_tr.shape, X_te.shape, "proporsi churn:", yc.mean().round(3))
[10]
penuh = DecisionTreeClassifier(random_state=0).fit(X_tr, y_tr)
print("Pohon penuh  — kedalaman:", penuh.get_depth(), "daun:", penuh.get_n_leaves())
print(f"  akurasi latih = {accuracy_score(y_tr, penuh.predict(X_tr)):.3f}")
print(f"  akurasi uji   = {accuracy_score(y_te, penuh.predict(X_te)):.3f}")

Overfitting: akurasi vs. kedalaman

[11]
hasil = []
for d in range(1, 21):
    mdl = DecisionTreeClassifier(max_depth=d, random_state=0).fit(X_tr, y_tr)
    hasil.append((d, accuracy_score(y_tr, mdl.predict(X_tr)), accuracy_score(y_te, mdl.predict(X_te))))
h = pd.DataFrame(hasil, columns=["max_depth", "latih", "uji"]).set_index("max_depth")
h.plot(marker="o", title="Akurasi vs. max_depth")
plt.show()
print("max_depth terbaik (uji):", h["uji"].idxmax())

Post-pruning dengan ccp_alpha

[12]
path = penuh.cost_complexity_pruning_path(X_tr, y_tr)
alphas = path.ccp_alphas[:-1]
skor = [accuracy_score(y_te, DecisionTreeClassifier(ccp_alpha=a, random_state=0)
                       .fit(X_tr, y_tr).predict(X_te)) for a in alphas]
terbaik = alphas[int(np.argmax(skor))]
plt.plot(alphas, skor, marker=".")
plt.xlabel("ccp_alpha"); plt.ylabel("akurasi uji"); plt.title("Post-pruning")
plt.show()
print(f"ccp_alpha terbaik = {terbaik:.5f}, akurasi uji = {max(skor):.3f}")
[13]
final = DecisionTreeClassifier(max_depth=4, random_state=0).fit(X_tr, y_tr)
plt.figure(figsize=(18, 7))
plot_tree(final, feature_names=Xc.columns, class_names=["stay", "churn"], filled=True,
          rounded=True, fontsize=8)
plt.show()
[14]
pd.Series(final.feature_importances_, index=Xc.columns).sort_values().plot(
    kind="barh", title="Feature importance (max_depth=4)")
plt.show()

✍️ Coba sendiri

  1. Latih pohon dengan criterion="gini" vs "entropy" (max_depth=4). Apakah akurasinya berbeda?
  2. Tambahkan kolom id_pelanggan unik ke data Play Tennis. Bandingkan information gain dan gain ratio-nya.
  3. Ekstrak 3 aturan IF-THEN dari pohon churn final (gunakan export_text). Terjemahkan ke strategi retensi.
  4. Lengkapi id3() di exercise.py dan bandingkan pohon yang dihasilkan dengan materi.
[15]
# 1.
[16]
# 2.

Aturan & strategi no. 3:

  • ...

Selanjutnya

Kerjakan kuis dan exercise.py, lalu jalankan python check.py 6 dari folder utama.