Chapter Feature Selection dengan Metode Embedded menutup pembahasan feature_importances_ dengan sebuah janji: importance berbasis impurity cenderung terlalu menghargai fitur dengan banyak nilai unik, dan permutation_importance adalah alternatif yang lebih adil — pembahasannya menunggu di section ini. Permutation importance menjawab pertanyaan yang sangat praktis: jika informasi fitur ini diambil dari model, seberapa jauh kinerja model jatuh? Pertanyaan itu dijawab bukan dengan membuka isi pohon, melainkan dengan eksperimen acak terkendali pada data — sehingga teknik ini berlaku untuk model apa pun, dari RandomForest sampai jaringan saraf. Chapter ini membedah mekanismenya, mengadilinya terhadap MDI (mean decrease in impurity) pada data yang kebenarannya kita ketahui, lalu berhadapan jujur dengan kelemahan terpentingnya: fitur yang saling berkorelasi.

Mekanisme: Memutus Hubungan Fitur dengan Target #

Ide dasarnya sederhana dan bisa digambar tanpa rumus. Pilih satu kolom fitur, acak urutan nilainya di seluruh baris, lalu prediksi ulang dan ukur skor. Pengacakan ini memutus hubungan kolom tersebut dengan target sekaligus dengan fitur lainnya — nilai yang tadinya sebaris dengan labelnya kini dipindahkan ke baris acak — sementara distribusi marginalnya (rerata, sebaran, rentang) tidak berubah sama sekali. Skor yang tidak turun berarti model tidak benar-benar memakai informasi kolom itu; skor yang jatuh dalam menunjukkan seberapa besar model bergantung padanya.

Karena satu pengacakan saja bisa sial, permutation_importance mengulang proses itu n_repeats kali dan melaporkan rata-rata beserta simpangan bakunya — rerata yang stabil dengan sebaran sempit berarti pentingnya fitur itu konsisten, bukan kebetulan. Tiga keputusan desain ada di tangan kita: scoring menentukan kaca yang dipakai (akurasi, F1, ROC-AUC, atau metrik regresi), dataset pembanding menentukan pertanyaan yang dijawab, dan modelnya tidak perlu dibuka sama sekali — cukup bisa dipanggil predict.

AspekMDI (feature_importances_)Permutasi (data latih)Permutasi (data uji)
Sumber nilaiPenurunan impurity saat pelatihanJatuhnya skor setelah pengacakanJatuhnya skor setelah pengacakan
Dataset dasarData latih (tempat model overfit)Data latihData yang belum dipakai belajar
Bias kardinalitas tinggiAda — banyak nilai unik menawarkan banyak pemisahanTidak adaTidak ada
Berlaku untuk modelHanya berbasis pohonModel apa punModel apa pun
Pertanyaan yang dijawabFitur apa yang dipakai membangun pohonFitur apa yang model andalkan di data yang dikenalnyaFitur apa yang benar-benar menopang generalisasi

Contoh 1: Tiga Lensa pada Data Berumpan #

Data Kredit Ceria dari chapter sebelumnya kita pakai lagi dengan satu tambahan yang meresahkan: id_pengajuan bernomor urut 0 sampai 499 — unik di tiap baris, tanpa makna apa pun, dan kardinalitas maksimum. Bersama umur (yang memang tidak dipakai mekanisme pembangkit), kita menanam dua fitur umpan yang seharusnya terbaca kosong. RandomForest 300 pohon dilatih, lalu tiga lensa importance diarahkan padanya: MDI dari properti feature_importances_, permutasi pada data latih, dan permutasi pada data uji.

python
# Tiga lensa importance pada data pengajuan pinjaman "Kredit Ceria":
# 1) MDI = feature_importances_ RandomForest (berbasis impurity, dihitung
#    pada data latih), 2) permutasi pada data latih, 3) permutasi pada
#    data uji. Dua fitur sengaja tanpa makna: umur dan id_pengajuan
#    (nomor unik per baris, kardinalitas maksimum).

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(42)
n = 500

pendapatan = rng.lognormal(mean=np.log(9.0), sigma=0.45, size=n)  # juta/bulan
rasio_utang = rng.uniform(0.05, 0.95, size=n)                     # cicilan/penghasilan
lama_kerja = rng.uniform(0.0, 20.0, size=n)                       # tahun
umur = rng.uniform(23.0, 60.0, size=n)                            # TIDAK dipakai target
riwayat_telat = np.clip(rng.poisson(1.2, size=n), 0, 12)          # jumlah telat bayar
id_pengajuan = np.arange(n)                                       # unik per baris, tanpa makna

logit = (2.6 * (np.log(pendapatan) - np.log(9.0)) / 0.45
         - 9.0 * (rasio_utang - 0.5)
         + 0.13 * (lama_kerja - 10.0)
         - 0.55 * (riwayat_telat - 1.2)
         + rng.normal(scale=0.8, size=n))
disetujui = (logit > 0).astype(int)

fitur = ["pendapatan", "rasio_utang", "lama_kerja", "umur",
         "riwayat_telat", "id_pengajuan"]
X = pd.DataFrame({
    "pendapatan": pendapatan,
    "rasio_utang": rasio_utang,
    "lama_kerja": lama_kerja,
    "umur": umur,
    "riwayat_telat": riwayat_telat,
    "id_pengajuan": id_pengajuan,
})[fitur]

X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, disetujui, test_size=0.25, random_state=42)

forest = RandomForestClassifier(
    n_estimators=300, random_state=42, n_jobs=1).fit(X_latih, y_latih)

mdi = forest.feature_importances_
perm_latih = permutation_importance(
    forest, X_latih, y_latih, scoring="accuracy",
    n_repeats=20, random_state=42, n_jobs=1)
perm_uji = permutation_importance(
    forest, X_uji, y_uji, scoring="accuracy",
    n_repeats=20, random_state=42, n_jobs=1)

print(f"Akurasi RandomForest: latih {forest.score(X_latih, y_latih):.3f}, "
      f"uji {forest.score(X_uji, y_uji):.3f}")
print(f"\n{'fitur':>14} | {'MDI':>6} | {'permutasi-latih':>16} | {'permutasi-uji':>13}")
for i in np.argsort(-mdi):
    print(f"{fitur[i]:>14} | {mdi[i]:6.3f} | {perm_latih.importances_mean[i]:16.3f} | "
          f"{perm_uji.importances_mean[i]:13.3f}")

derau = [fitur.index("umur"), fitur.index("id_pengajuan")]
print("\nTotal bobot pada dua fitur tanpa makna (umur + id_pengajuan):")
print(f"  MDI               : {mdi[derau].sum():.3f}")
print(f"  permutasi (latih) : {perm_latih.importances_mean[derau].sum():.3f}")
print(f"  permutasi (uji)   : {perm_uji.importances_mean[derau].sum():.3f}")

Output yang Dihasilkan:

text
Akurasi RandomForest: latih 1.000, uji 0.896

         fitur |    MDI |  permutasi-latih | permutasi-uji
    pendapatan |  0.414 |            0.312 |         0.253
   rasio_utang |  0.330 |            0.276 |         0.222
    lama_kerja |  0.094 |            0.039 |         0.044
          umur |  0.068 |            0.007 |         0.001
  id_pengajuan |  0.066 |            0.008 |         0.011
 riwayat_telat |  0.028 |            0.002 |         0.003

Total bobot pada dua fitur tanpa makna (umur + id_pengajuan):
  MDI               : 0.134
  permutasi (latih) : 0.015
  permutasi (uji)   : 0.012

Mari kita telaah baris-baris kunci pada contoh di atas:

  1. Baris 21 (id_pengajuan = np.arange(n)): nomor urut 0–499 memberi setiap baris nilai unik — keadaan terburuk bagi MDI dan umpan paling telanjang bagi kedua metode.
  2. Baris 47 (mdi = forest.feature_importances_): MDI dibaca langsung dari properti model; tidak ada dataset baru yang ikut main, seluruh nilai lahir dari proses pelatihan di data latih.
  3. Baris 48–53 (dua pemanggilan permutation_importance): pemanggilan pertama mengacak kolom pada data latih, pemanggilan kedua pada data uji — parameter n_repeats=20 dan random_state=42 menjaga hasil stabil dan dapat direproduksi.
  4. Baris 62–66 (rekapitulasi derau): derau mengumpulkan indeks kedua fitur umpan lalu menjumlahkan bobotnya di bawah ketiga lensa — angka ringkas yang menjadi hakim utama demo ini.

Keluarannya menghukum MDI dengan telak. Dua fitur tanpa makna menyerap 0,134 MDI — 13,4% dari seluruh bobot — padahal permutasi di data uji menaksir kontribusi keduanya hanya 0,012. MDI bukan sedikit meleset; nilai kepemilikannya atas fitur yang kita tahu kosong sebelas kali lipat lebih besar dari kenyataannya. Penyebabnya dua lapis: MDI dihitung di data latih tempat model mencapai akurasi sempurna 1,000 (pohon sempat menghafal), dan fitur dengan ratusan nilai unik menawarkan ratusan kandidat pemisahan yang menguntungkan secara kebetulan. Perhatikan juga lama_kerja: MDI menilai 0,094 sementara permutasi uji berkata 0,044 — fitur yang sungguh berinformasi pun digelembungkan karena dihitung di wilayah hafalan. Pembedaan permutasi-latih versus permutasi-uji pada data ini memang tipis (0,015 lawan 0,012 untuk fitur umpan), tetapi prinsipnya tetap: hanya pengukuran di data yang tidak dipakai belajar yang berhak menjawab “fitur apa yang menopang generalisasi”.

Contoh 2: Fitur Berkorelasi Membagi Importance #

Kejujuran permutasi punya harga. Ketika dua fitur membawa informasi yang hampir sama — gaji peminjam dan gaji pasangannya sama-sama merupakan pengamatan bising atas satu faktor kelayakan yang sama — model bisa memakai salah satunya tanpa kehilangan banyak. Akibatnya, mengacak satu kolom hanya menjatuhkan skor sebagian, karena kembarannya masih menyuplai informasi pengganti. Importance tidak hilang; ia terbagi di antara para proksi. Demo berikut mengukur pembagian itu dengan membandingkan model yang melihat dua proksi melawan model yang hanya melihat satu.

python
# Fitur berkorelasi membagi nilai importance: dua kolom gaji adalah proksi
# dari satu faktor laten yang sama. Membandingkan model yang melihat dua
# proksi vs model yang hanya melihat satu menunjukkan bahwa importance
# "terbagi" di antara fitur yang saling menggantikan.

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.inspection import permutation_importance
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(42)
n = 600

# Satu faktor tersembunyi menentukan kelayakan; dua kolom gaji sama-sama
# merupakan pengamatan tidak sempurna atas faktor tersebut.
laten = rng.normal(size=n)
gaji = np.clip(8.0 + 2.5 * laten + rng.normal(scale=0.9, size=n), 0.5, None)
gaji_pasangan = np.clip(8.0 + 2.5 * laten + rng.normal(scale=0.9, size=n), 0.5, None)
usia = rng.uniform(23.0, 60.0, size=n)  # fitur derau

logit = 1.1 * laten + rng.normal(scale=0.9, size=n)
disetujui = (logit > 0).astype(int)

korelasi = np.corrcoef(gaji, gaji_pasangan)[0, 1]

df = pd.DataFrame({
    "gaji": gaji,
    "gaji_pasangan": gaji_pasangan,
    "usia": usia,
    "disetujui": disetujui,
})
X_latih, X_uji, y_latih, y_uji = train_test_split(
    df[["gaji", "gaji_pasangan", "usia"]], df["disetujui"],
    test_size=0.25, random_state=42)

# Model A: melihat kedua proksi (keadaan dunia nyata).
model_dua = RandomForestClassifier(
    n_estimators=300, random_state=42, n_jobs=1).fit(X_latih, y_latih)

# Model B: hanya melihat satu proksi -- pembanding untuk importance penuh.
model_satu = RandomForestClassifier(
    n_estimators=300, random_state=42, n_jobs=1).fit(X_latih[["gaji", "usia"]], y_latih)

perm_dua = permutation_importance(
    model_dua, X_uji, y_uji, scoring="accuracy",
    n_repeats=20, random_state=42, n_jobs=1)
perm_satu = permutation_importance(
    model_satu, X_uji[["gaji", "usia"]], y_uji, scoring="accuracy",
    n_repeats=20, random_state=42, n_jobs=1)

print(f"Korelasi gaji vs gaji_pasangan: {korelasi:.3f}")
print(f"Akurasi uji model dua proksi : {model_dua.score(X_uji, y_uji):.3f}")
print(f"Akurasi uji model satu proksi: {model_satu.score(X_uji[['gaji', 'usia']], y_uji):.3f}")

print("\nPermutation importance model DUA proksi (data uji):")
for nama, nilai in zip(["gaji", "gaji_pasangan", "usia"], perm_dua.importances_mean):
    print(f"  {nama:>14}: {nilai:+.3f}")
pasangan_total = perm_dua.importances_mean[0] + perm_dua.importances_mean[1]

print("\nPermutation importance model SATU proksi (data uji):")
for nama, nilai in zip(["gaji", "usia"], perm_satu.importances_mean):
    print(f"  {nama:>14}: {nilai:+.3f}")

print(f"\nJumlah importance gaji + gaji_pasangan (model dua) : {pasangan_total:+.3f}")
print(f"Importance gaji saat sendirian (model satu)        : {perm_satu.importances_mean[0]:+.3f}")
print("Kedua angka di atas seharusnya berdekatan: importance faktor laten")
print("tidak hilang, hanya terbagi di antara kedua proksinya.")

Output yang Dihasilkan:

text
Korelasi gaji vs gaji_pasangan: 0.874
Akurasi uji model dua proksi : 0.760
Akurasi uji model satu proksi: 0.720

Permutation importance model DUA proksi (data uji):
            gaji: +0.093
   gaji_pasangan: +0.081
            usia: +0.000

Permutation importance model SATU proksi (data uji):
            gaji: +0.189
            usia: -0.020

Jumlah importance gaji + gaji_pasangan (model dua) : +0.174
Importance gaji saat sendirian (model satu)        : +0.189
Kedua angka di atas seharusnya berdekatan: importance faktor laten
tidak hilang, hanya terbagi di antara kedua proksinya.

Empat bagian kode patut digarisbawahi. Baris 17–19 membangun dua pengamatan biring atas satu faktor laten — keduanya berkorelasi 0,874 menurut baris 25, dan hanya laten (bukan kolom gaji) yang menentukan target di baris 22. Baris 37–43 melatih dua model pada data latih yang sama: model_dua melihat kedua proksi seperti dunia nyata, model_satu hanya melihat gaji sebagai pembanding tunggal. Baris 45–50 menghitung permutation importance di data uji untuk kedua model. Baris 65–68 menutup dengan perbandingan yang menjadi inti demo: jumlah importance kedua proksi versus importance gaji ketika sendirian.

Angka-angkanya bercerita konsisten. Pada model dua proksi, gaji (+0,093) dan gaji_pasangan (+0,081) masing-masing tampak hanya separuh penting — pembaca yang terburu-buru akan salah menyimpulkan bahwa gaji nasabah “tidak terlalu berpengaruh” pada keputusan kredit. Jumlah keduanya, +0,174, hampir menyamai importance gaji saat sendirian (+0,189): informasi faktor laten tidak hilang, ia cuma terbagi. Menariknya, model dua proksi justru lebih akurat (0,760 lawan 0,720) — dua pengamatan biring yang digabung pohon memang lebih baik daripada satu — sehingga membuang salah satu kolom demi “menyederhanakan” berdasarkan angka importance akan turut menurunkan kinerja. Pelajaran praktisnya: sebelum menjatuhkan fitur bernilai rendah, periksa apakah fitur itu punya kembaran berkorelasi; bila ya, evaluasilah sebagai kelompok atau satukan dulu menjadi satu fitur gabungan (misalnya total gaji rumah tangga) baru diukur ulang.

Praktik Terbaik Membaca Angka Permutasi #

Empat kebiasaan membuat angka-angka ini layak dipertanggungjawabkan. Pertama, ukur di data yang tidak dipakai belajar — data uji atau validasi — karena pertanyaan bisnis selalu tentang generalisasi. Kedua, sebutkan scoring dan datasetnya saat melaporkan: importance akurasi pada data uji bulan Maret adalah pernyataan yang jauh lebih kuat (dan lebih sempit) daripada “fitur penting”. Ketiga, pakai n_repeats cukup besar (10–20 ke atas) dan baca importances_std — fitur yang importancenya goyah antar-pengulangan belum layak dipakai mengambil keputusan. Keempat, perlakukan hasil sebagai deskriptif, bukan preskriptif: importance tinggi berarti model memakai fitur itu, bukan bahwa hubungannya sebab-akibat — tema yang akan dituntaskan di Section Causal Machine Learning.

Dengan daftar fitur terurut di tangan, pertanyaan berikutnya langsung muncul: bentuk hubungannya seperti apa? Pendapatan yang penting — apakah pengaruhnya menanjak lurus, melengkung jenuh, atau berbalik arah pada nilai tertentu? Importance tidak bisa menjawabnya; alat yang menggambar bentuk hubungan itulah bintang Chapter Partial Dependence dan ICE.

Referensi Resmi #