Regresi logistik pada chapter sebelumnya memisahkan dua kelas dengan satu garis lurus. Namun aturan layanan pelanggan di dunia nyata jarang sehemat itu: Toko Kopi Kita memberi status prioritas bila nilai belanja bulanan di atas 500 ribu rupiah, atau bila nilai belanja 300-500 ribu tetapi frekuensi kunjungan enam kali ke atas — aturan bertingkat, bukan garis. Decision tree adalah model yang mempelajari aturan bertingkat semacam itu sendiri: serangkaian pertanyaan ya/tidak yang berakhir pada satu keputusan. Pada chapter ini kita melatih pohon dengan scikit-learn, membaca aturannya baris demi baris, lalu mengendalikan risiko terbesarnya — kecenderungan tumbuh terlalu dalam hingga menghafal data.

Cara Pohon Bekerja: Pertanyaan Berurutan #

Sebuah decision tree adalah diagram alur yang model bangun sendiri dari data. Mulai dari satu pertanyaan di puncak pohon — misalnya “nilai belanja <= 270 ribu?” — lalu tiap jawaban membuka pertanyaan berikutnya hingga berhenti di daun (leaf) yang menyatakan kelas. Selama pelatihan, algoritma mencoba berbagai pertanyaan pemisah pada tiap simpul dan memilih yang paling memurnikan kotak data — kotak yang isinya makin satu kelas saja, diukur dengan ukuran ketidakmurnian Gini impurity; angka 0 berarti kotak berisi satu kelas murni.

Prosesnya rakus dan bertingkat, persis cara kita mengelompokkan kertas: belah tumpukan di pertanyaan termurah dulu, lalu belah lagi tumpukan yang masih campuran. Tiga konsekuensi penting mengikuti bentuk kerja ini:

Karakter decision treeAkibatnya
Pertanyaan pada satu fitur dalam satu waktuBatas keputusan berbentuk undakan, bukan garis miring
Pertanyaan diambil dari nilai data nyataFitur numerik langsung dipakai tanpa perlu penskalaan
Pohon bisa terus membelah sampai kotak murniSempurna di data latih, rawan menghafal derau

Poin terakhir itulah yang akan kita buktikan dengan angka sebentar lagi. Sebelum itu, siapkan datanya.

Melatih Pohon Kelas Layanan #

Data kita berupa 34 pelanggan Toko Kopi Kita dengan dua fitur — nilai_belanja (ribu rupiah per bulan) dan frekuensi_kunjungan (kali per bulan) — dan target prioritas: 1 berarti masuk kelas layanan prioritas, 0 berarti reguler. Dua baris sengaja dibuat melawan aturan umum (pelanggan besar yang tetap reguler, pelanggan kecil yang justru diprioritaskan) agar data tidak sempurna terpisah, sebagaimana data nyata:

python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier

pelanggan = pd.DataFrame({
    "nilai_belanja": [700, 490, 620, 760, 350, 650, 380, 310, 280, 420, 240, 380,
                      550, 450, 220, 400, 330, 290, 720, 180, 200, 320, 250, 600,
                      90, 480, 520, 510, 120, 360, 500, 440, 150, 580],
    "frekuensi_kunjungan": [5, 5, 8, 9, 9, 10, 4, 3, 2, 4, 9, 8,
                            4, 6, 7, 10, 6, 3, 4, 6, 5, 7, 3, 6,
                            4, 7, 6, 2, 2, 2, 3, 3, 8, 7],
    "prioritas": [1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1,
                  1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1,
                  0, 1, 0, 1, 0, 0, 1, 0, 0, 1],
})

fitur = ["nilai_belanja", "frekuensi_kunjungan"]
X = pelanggan[fitur]
y = pelanggan["prioritas"]

X_latih, X_uji, y_latih, y_uji = train_test_split(X, y, test_size=0.3,
                                                   stratify=y, random_state=42)
model = DecisionTreeClassifier(random_state=42)
model.fit(X_latih, y_latih)

print("Kedalaman pohon    : %d" % model.get_depth())
print("Jumlah daun        : %d" % model.get_n_leaves())
print("Akurasi data latih : %.2f" % model.score(X_latih, y_latih))
print("Akurasi data uji   : %.2f" % model.score(X_uji, y_uji))

Output yang Dihasilkan:

text
Kedalaman pohon    : 6
Jumlah daun        : 8
Akurasi data latih : 1.00
Akurasi data uji   : 0.82

Mari kita telaah baris-baris kode di atas:

  1. DecisionTreeClassifier(random_state=42): pohon secara bawaan tumbuh sampai setiap daun murni atau data habis — tidak ada parameter wajib. random_state mengunci pilihan saat beberapa pemisah sama bagusnya, agar pohon yang tumbuh selalu identik.
  2. model.get_depth() dan model.get_n_leaves() melaporkan bentuk pohon: 6 tingkat pertanyaan, 8 daun keputusan. Kedalaman ini sebanding dengan kompleksitas aturan yang dipelajari.
  3. Akurasi latih 1,00 tetapi uji 0,82 — jurang 18 poin persen. Pohon berhasil menghafal seluruh 23 pelanggan latih, termasuk dua pengecualian tadi, lalu meleset pada 2 dari 11 pelanggan uji. Inilah overfitting yang dijanjikan: kotak-kotak yang dibelah terlalu halus hanya berisi satu-dua baris dan mulai menggeneralisasi dari kebetulan.

Membaca Aturan Pohon #

Keunggulan terbesar decision tree adalah hasilnya bisa dibaca langsung sebagai aturan. Fungsi export_text() menerjemahkan pohon menjadi deretan if-else:

python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

pelanggan = pd.DataFrame({
    "nilai_belanja": [700, 490, 620, 760, 350, 650, 380, 310, 280, 420, 240, 380,
                      550, 450, 220, 400, 330, 290, 720, 180, 200, 320, 250, 600,
                      90, 480, 520, 510, 120, 360, 500, 440, 150, 580],
    "frekuensi_kunjungan": [5, 5, 8, 9, 9, 10, 4, 3, 2, 4, 9, 8,
                            4, 6, 7, 10, 6, 3, 4, 6, 5, 7, 3, 6,
                            4, 7, 6, 2, 2, 2, 3, 3, 8, 7],
    "prioritas": [1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1,
                  1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1,
                  0, 1, 0, 1, 0, 0, 1, 0, 0, 1],
})
X = pelanggan[["nilai_belanja", "frekuensi_kunjungan"]]
y = pelanggan["prioritas"]
X_latih, X_uji, y_latih, y_uji = train_test_split(X, y, test_size=0.3,
                                                   stratify=y, random_state=42)
model = DecisionTreeClassifier(random_state=42)
model.fit(X_latih, y_latih)

print(export_text(model, feature_names=["nilai_belanja", "frekuensi_kunjungan"]))

Output yang Dihasilkan:

text
|--- nilai_belanja <= 270.00
|   |--- class: 0
|--- nilai_belanja >  270.00
|   |--- frekuensi_kunjungan <= 6.50
|   |   |--- nilai_belanja <= 535.00
|   |   |   |--- nilai_belanja <= 300.00
|   |   |   |   |--- class: 1
|   |   |   |--- nilai_belanja >  300.00
|   |   |   |   |--- frekuensi_kunjungan <= 5.50
|   |   |   |   |   |--- nilai_belanja <= 500.00
|   |   |   |   |   |   |--- class: 0
|   |   |   |   |   |--- nilai_belanja >  500.00
|   |   |   |   |   |   |--- class: 1
|   |   |   |   |--- frekuensi_kunjungan >  5.50
|   |   |   |   |   |--- nilai_belanja <= 485.00
|   |   |   |   |   |   |--- class: 1
|   |   |   |   |   |--- nilai_belanja >  485.00
|   |   |   |   |   |   |--- class: 0
|   |   |--- nilai_belanja >  535.00
|   |   |   |--- class: 1
|   |--- frekuensi_kunjungan >  6.50
|   |   |--- class: 1

Cara membaca aturan tersebut dari atas ke bawah, mengikuti satu jalur pada satu waktu:

  1. Pertanyaan pertama membelah di angka 270 ribu — model sendiri menemukan bahwa pelanggan di bawah sana nyaris pasti reguler, mendekati batas 300 ribu pada aturan bisnis aslinya. Angka 270 diambil dari nilai-nilai data, bukan dibulatkan; pohon tidak tahu manusia suka angka bulat.
  2. Cabang kanan bawah (frekuensi_kunjungan > 6.50) langsung memutuskan prioritas — menangkap bagian kedua aturan bisnis: pengunjung rajin di kelas menengah naik kelas.
  3. Empat daun terdalam di tengah adalah bekas dua pengecualian kita: belahan nilai <= 300 → class 1 ada karena satu pelanggan 290 ribu yang diprioritaskan, dan belahan 485 < nilai <= 535 → class 0 mewadahi pelanggan 520 ribu yang tetap reguler. Dengan hanya satu-dua baris di dalamnya, daun-daun ini menghafal, bukan merumuskan — sumber utama jurang akurasi latih-uji tadi.

Mengendalikan Kedalaman dengan max_depth #

Karena pohon yang dibiarkan tumbuh penuh cenderung menghafal, kita memangkasnya dengan max_depth — jumlah tingkat pertanyaan maksimum yang boleh dibuat. Kita bandingkan tiga pohon pada data yang sama:

python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

pelanggan = pd.DataFrame({
    "nilai_belanja": [700, 490, 620, 760, 350, 650, 380, 310, 280, 420, 240, 380,
                      550, 450, 220, 400, 330, 290, 720, 180, 200, 320, 250, 600,
                      90, 480, 520, 510, 120, 360, 500, 440, 150, 580],
    "frekuensi_kunjungan": [5, 5, 8, 9, 9, 10, 4, 3, 2, 4, 9, 8,
                            4, 6, 7, 10, 6, 3, 4, 6, 5, 7, 3, 6,
                            4, 7, 6, 2, 2, 2, 3, 3, 8, 7],
    "prioritas": [1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1,
                  1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1,
                  0, 1, 0, 1, 0, 0, 1, 0, 0, 1],
})
X = pelanggan[["nilai_belanja", "frekuensi_kunjungan"]]
y = pelanggan["prioritas"]
X_latih, X_uji, y_latih, y_uji = train_test_split(X, y, test_size=0.3,
                                                   stratify=y, random_state=42)

print("max_depth | kedalaman | daun | akurasi latih | akurasi uji")
for kedalaman in [2, 3, None]:
    model = DecisionTreeClassifier(max_depth=kedalaman, random_state=42)
    model.fit(X_latih, y_latih)
    label = "penuh" if kedalaman is None else str(kedalaman)
    print("%9s | %10d | %4d | %13.2f | %11.2f" % (
        label, model.get_depth(), model.get_n_leaves(),
        model.score(X_latih, y_latih), model.score(X_uji, y_uji)))

model3 = DecisionTreeClassifier(max_depth=3, random_state=42)
model3.fit(X_latih, y_latih)
print()
print(export_text(model3, feature_names=["nilai_belanja", "frekuensi_kunjungan"]))

Output yang Dihasilkan:

text
max_depth | kedalaman | daun | akurasi latih | akurasi uji
        2 |          2 |    3 |          0.74 |        0.73
        3 |          3 |    4 |          0.83 |        0.91
    penuh |          6 |    8 |          1.00 |        0.82

|--- nilai_belanja <= 270.00
|   |--- class: 0
|--- nilai_belanja >  270.00
|   |--- frekuensi_kunjungan <= 6.50
|   |   |--- nilai_belanja <= 535.00
|   |   |   |--- class: 0
|   |   |--- nilai_belanja >  535.00
|   |   |   |--- class: 1
|   |--- frekuensi_kunjungan >  6.50
|   |   |--- class: 1

Tabel tersebut memperlihatkan tiga watak model dalam satu deret:

  1. max_depth=2 terlalu pendek (underfitting): akurasi latih dan uji sama-sama rendah di 0,74/0,73 — pohon tidak sanggup menyimpan aturan bertingkat lengkapnya.
  2. max_depth=3 paling seimbang: akurasi uji tertinggi 0,91, mengalahkan pohon penuh! Empat daunnya cukup menampung aturan bisnis inti, dan dua pengecualian yang tidak terwadahi menjadi kesalahan yang bisa ditolelir.
  3. Pohon penuh menuai apa yang ditanamnya: belahan tambahan demi akurasi latih sempurna justru menurunkan akurasi uji. Aturan depth-3 juga jauh lebih mudah dibaca — empat pertanyaan yang bisa dipasang di dinding kasir.

Melihat Wujud Pohonnya #

Blok terakhir menggambar pohon depth-3 dengan plot_tree():

python
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree

pelanggan = pd.DataFrame({
    "nilai_belanja": [700, 490, 620, 760, 350, 650, 380, 310, 280, 420, 240, 380,
                      550, 450, 220, 400, 330, 290, 720, 180, 200, 320, 250, 600,
                      90, 480, 520, 510, 120, 360, 500, 440, 150, 580],
    "frekuensi_kunjungan": [5, 5, 8, 9, 9, 10, 4, 3, 2, 4, 9, 8,
                            4, 6, 7, 10, 6, 3, 4, 6, 5, 7, 3, 6,
                            4, 7, 6, 2, 2, 2, 3, 3, 8, 7],
    "prioritas": [1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1,
                  1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1,
                  0, 1, 0, 1, 0, 0, 1, 0, 0, 1],
})
X = pelanggan[["nilai_belanja", "frekuensi_kunjungan"]]
y = pelanggan["prioritas"]
X_latih, X_uji, y_latih, y_uji = train_test_split(X, y, test_size=0.3,
                                                   stratify=y, random_state=42)

model = DecisionTreeClassifier(max_depth=3, random_state=42)
model.fit(X_latih, y_latih)

plt.figure(figsize=(9, 5))
plot_tree(model, feature_names=["nilai_belanja", "frekuensi_kunjungan"],
          class_names=["reguler", "prioritas"], filled=True, rounded=True, fontsize=9)
plt.tight_layout()
plt.show()

Gambar yang dihasilkan menampilkan pohon terbalik: akar di atas berlabel pertanyaan pertama nilai_belanja <= 270, dua anaknya membawa cabang benar-salah, dan empat kotak daun di dasar diberi warna sesuai kelasnya — oranye untuk reguler, biru untuk prioritas, dengan pekatnya warna menunjukkan kemurnian kelas di kotak itu. Setiap simpul juga mencantumkan jumlah sampel yang mengalir ke sana, sehingga kita bisa melihat bahwa daun class: 0 di kedalaman tiga hanya memuat segelintir pelanggan menengah yang jarang datang.

Kekuatan dan Kelemahan Decision Tree #

AspekKeterangan
Aturan bisa dibaca manusiaexport_text() menghasilkan if-else yang bisa dipasang di SOP
Batas keputusan bebas bentukUndakan, kotak, wilayah bertingkat — bukan garis lurus
Tanpa penskalaan fiturPertanyaan pada satu fitur tidak terpengaruh skala fitur lain
Sangat rawan overfittingPohon penuh menghafal hingga akurasi latih 1,00
Tidak stabilPerubahan kecil pada data bisa mengubah seluruh struktur pohon
Bias belahan bertingkatKotak-kotak sejajar sumbu kurang cocok untuk batas miring

Kapan Kita Memakainya #

Decision tree tepat ketika hasil harus bisa dijelaskan sebagai aturan — kebijakan layanan, kelayakan kredit ringan, triase keluhan pelanggan. Ia juga jadi titik awal yang baik saat eksplorasi: satu pohon depth-3 sering langsung memperlihatkan fitur mana yang benar-benar menentukan. Kelemahannya yang terukur — tidak stabil dan gampang menghafal — justru menjadi alasan munculnya gagasan ensemble: kalau satu pohon mudah goyah, tanam seribu pohon dan ambil suara mayoritasnya.

Rangkuman #

  • Decision tree membangun aturan ya/tidak bertingkat; tiap simpul memilih pertanyaan yang paling menurunkan Gini impurity, dan daun menyatakan kelas.
  • get_depth() dan get_n_leaves() melaporkan kompleksitas pohon; pohon penuh kita mencapai kedalaman 6 dengan 8 daun.
  • Akurasi latih 1,00 berbanding uji 0,82 membuktikan overfitting: belahan terdalam menghafal pengecualian, bukan pola.
  • export_text() menerjemahkan pohon menjadi aturan if-else yang bisa dibaca dan diperbandingkan dengan logika bisnis asli.
  • max_depth=3 menghasilkan pohon terbaik (akurasi uji 0,91) sekaligus paling mudah dibaca — kompleksitas dikendalikan, bukan dibiarkan.

Satu pohon terbaik pun tetap goyah bila data berganti. Solusinya bukan memangkas lebih dalam, melainkan menanam banyak pohon sekaligus dan mendengarkan suara mayoritasnya — itulah ide di balik Random Forest .

Referensi #

  1. scikit-learn — FAQ (memilih dan menyetel model)
  2. scikit-learn — train_test_split
  3. scikit-learn — Model evaluation