SHAP menutup chapter sebelumnya dengan angka: kontribusi −0,298 dari pendapatan Budi, +0,237 dari rasio utang Siti, terverifikasi aditif hingga tingkat pembulatan mesin. Tetapi daftar bilangan tetap bukan bahasa kerja tim risiko dan regulator — mereka membaca dunia lewat aturan: tolak bila rasio utang tinggi dan pendapatan rendah. Surrogate model menjembatani dua bahasa itu dengan cara yang terang-terangan: latih model sederhana yang bisa dibaca untuk meniru prediksi kotak hitam, lalu ukur seberapa setia tiruan itu. Chapter penutup section ini mempraktikkan dua bentuknya — global berupa pohon aturan, lokal berupa regresi linier di sekitar satu pengajuan (ide di balik LIME) — sebelum berhenti jujur pada pertanyaan yang paling penting: seberapa jauh penjelasan atas sebuah penyalinan boleh dibaca sebagai penjelasan atas model aslinya?

Surrogate Global: Menyalin Kotak Hitam dengan Aturan #

Satu kalimat membedakan surrogate dari model biasa: target latihannya adalah prediksi kotak hitam, bukan label asli. RandomForest 300 pohon terlebih dulu dilatih pada data Kredit Ceria; keluarannya di 500 baris data latih menjadi y baru; DecisionTree dangkal belajar dari pasangan (X, prediksi forest) tersebut. Kualitas penyalinan diukur dengan fidelity — proporsi baris tempat surrogate dan forest bersepakat — bukan dengan akurasi terhadap kebenaran. Semakin dangkal pohonnya, semakin mudah dibaca; semakin dalam, semakin setia. Tabel fidelity sepanjang beberapa kedalaman adalah kurva trade-off yang harus dibaca sebelum memilih.

python
# Surrogate global: DecisionTree dangkal dilatih untuk MENIRU prediksi
# RandomForest "Kredit Ceria" (target latihan = prediksi forest, bukan label
# asli). Fidelity diukur pada data latih dan data uji; aturan pohon
# kedalaman 3 dicetak dengan export_text.

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, export_text

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

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"]
X = pd.DataFrame({
    "pendapatan": pendapatan,
    "rasio_utang": rasio_utang,
    "lama_kerja": lama_kerja,
    "umur": umur,
    "riwayat_telat": riwayat_telat,
})[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)

# Target surrogate adalah PREDIKSI forest -- inilah yang membedakan
# surrogate dari model biasa.
y_forest_latih = forest.predict(X_latih)
y_forest_uji = forest.predict(X_uji)

print("Fidelity DecisionTree sebagai peniru RandomForest:")
print(f"{'kedalaman':>9} | {'fidelity latih':>15} | {'fidelity uji':>13}")
for kedalaman in [1, 2, 3, 4, 5, 7]:
    pohon = DecisionTreeClassifier(
        max_depth=kedalaman, random_state=42).fit(X_latih, y_forest_latih)
    fid_latih = accuracy_score(y_forest_latih, pohon.predict(X_latih))
    fid_uji = accuracy_score(y_forest_uji, pohon.predict(X_uji))
    print(f"{kedalaman:9d} | {fid_latih:15.3f} | {fid_uji:13.3f}")

surrogate = DecisionTreeClassifier(
    max_depth=3, random_state=42).fit(X_latih, y_forest_latih)
print("\nAturan surrogate kedalaman 3 (export_text):")
print(export_text(surrogate, feature_names=fitur, decimals=2))

acc_surrogate = accuracy_score(y_uji, surrogate.predict(X_uji))
acc_forest = accuracy_score(y_uji, y_forest_uji)
print(f"Akurasi terhadap label asli (data uji): forest {acc_forest:.3f}, "
      f"surrogate {acc_surrogate:.3f}")
beda = int((surrogate.predict(X_uji) != y_forest_uji).sum())
print(f"Surrogate berbeda pendapat dengan forest pada {beda} dari {len(X_uji)} baris uji")
print("-- wilayah tempat aturan sederhana dan 300 pohon tidak sepakat.")

Output yang Dihasilkan:

text
Fidelity DecisionTree sebagai peniru RandomForest:
kedalaman |  fidelity latih |  fidelity uji
        1 |           0.776 |         0.736
        2 |           0.816 |         0.776
        3 |           0.909 |         0.880
        4 |           0.939 |         0.872
        5 |           0.965 |         0.904
        7 |           0.995 |         0.912

Aturan surrogate kedalaman 3 (export_text):
|--- pendapatan <= 8.39
|   |--- rasio_utang <= 0.26
|   |   |--- pendapatan <= 6.49
|   |   |   |--- class: 0
|   |   |--- pendapatan >  6.49
|   |   |   |--- class: 1
|   |--- rasio_utang >  0.26
|   |   |--- pendapatan <= 7.66
|   |   |   |--- class: 0
|   |   |--- pendapatan >  7.66
|   |   |   |--- class: 0
|--- pendapatan >  8.39
|   |--- rasio_utang <= 0.49
|   |   |--- riwayat_telat <= 4.50
|   |   |   |--- class: 1
|   |   |--- riwayat_telat >  4.50
|   |   |   |--- class: 0
|   |--- rasio_utang >  0.49
|   |   |--- pendapatan <= 13.00
|   |   |   |--- class: 0
|   |   |--- pendapatan >  13.00
|   |   |   |--- class: 1

Akurasi terhadap label asli (data uji): forest 0.880, surrogate 0.840
Surrogate berbeda pendapat dengan forest pada 15 dari 125 baris uji
-- wilayah tempat aturan sederhana dan 300 pohon tidak sepakat.

Baris-baris kuncinya ramping. Baris 46–47 membuat target penyalinan: forest.predict(X_latih) — perhatikan bahwa y_latih yang berisi label asli tidak dipakai di sini. Baris 51–56 menyapu enam kedalaman dan mencatat fidelity di dua dataset; baris 58–61 melatih pohon kedalaman 3 lalu mencetak aturannya dengan export_text — teks murni, tanpa gambar; baris 63–69 menutup dengan perbandingan akurasi terhadap label asli dan jumlah baris perselisihan.

Tabel fidelity mengajarkan pelajaran yang sama dengan Chapter Permutation Importance, kali ini untuk penyalinan: ukur di data yang tidak dipakai membangun surrogate. Fidelity latih monoton menanjak sampai 0,995 pada kedalaman 7 — tetapi fidelity uji berhenti di 0,912; pohon yang terlalu dalam mulai menghafal keanehan forest di data latih, bukan kebijakan forest secara umum (kedalaman 4 bahkan sempat tersendat ke 0,872 sebelum naik lagi — kestabilan antar kedalaman juga bagian dari data). Pilih kedalaman 3: fidelity uji 0,880 dengan dua belas baris aturan yang terbaca manusia. Aturannya pun masuk akal secara domain — pendapatan di bawah Rp8,39 juta dengan rasio utang di atas 0,26 ditolak; pendapatan di atasnya disetujui selama rasio utang di bawah 0,49 dan riwayat keterlambatan ringan. Dua kaki lagi layak dicatat: cabang rasio_utang > 0.26 pada pendapatan kecil menghasilkan kelas 0 di kedua daunnya — pemisahan yang tidak mengubah keputusan, sisa kehati-hatian pohon menyalin tepi hutan — dan angka penutupnya menohok: terhadap label asli, aturan dua belas baris ini mencapai akurasi 0,840 dari 0,880 milik forest. Sebagian besar “kecerdasan” hutan di data ini memang bisa ditulis sebagai aturan; yang tersisa (15 dari 125 baris uji) adalah wilayah perselisihan tempat keputusan sesungguhnya diambil.

Surrogate Lokal: Ide LIME dalam NumPy Murni #

Surrogate global memaksa satu aturan mewakili seluruh peta keputusan. LIME (Local Interpretable Model-agnostic Explanations) membaliknya: hanya salin lingkungan satu titik, karena di sanalah keputusan yang perlu dijelaskan. Resepnya tiga langkah — bangkitkan sampel gangguan di sekitar kasus target, timbang tiap sampel menurut kemiripannya dengan kasus itu, lalu latih model linier sederhana terbobot atas prediksi kotak hitam. Pustaka lime tidak terpasang di lingkungan Course ini, jadi kita implementasikan idenya langsung dengan NumPy dan scikit-learn — yang justru memperlihatkan mesinnya tanpa sulap pustaka.

python
# Surrogate lokal ala LIME, diimplementasikan manual dengan NumPy +
# scikit-learn (pustaka lime tidak dipasang): bangkitkan sampel gangguan
# di sekitar satu pengajuan, boboti menurut kemiripan, lalu latih model
# linier lokal atas probabilitas forest.

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
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

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"]
X = pd.DataFrame({
    "pendapatan": pendapatan,
    "rasio_utang": rasio_utang,
    "lama_kerja": lama_kerja,
    "umur": umur,
    "riwayat_telat": riwayat_telat,
})[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)

# Kasus target: pengajuan paling "batas" di data uji (probabilitas terdekat
# ke 0.5) -- keputusan yang paling membutuhkan justifikasi terperinci.
proba_uji = forest.predict_proba(X_uji)[:, 1]
i_budi = int(np.argsort(np.abs(proba_uji - 0.5))[0])
nilai_budi = X_uji.iloc[i_budi].to_numpy(dtype=float)
proba_budi = float(proba_uji[i_budi])

print("Pengajuan Budi (kasus paling batas di data uji):")
for nama, nilai in zip(fitur, nilai_budi):
    print(f"  {nama:>14} = {nilai:7.2f}")
print(f"  prediksi forest = {proba_budi:.3f}")

# 1) Sampel gangguan: tarik tiap fitur dari Normal(rata-rata, simpangan)
#    data latih (pendekatan LIME untuk data tabular kontinu).
m = 300
rata2 = X_latih.mean(axis=0).to_numpy()
simpangan = X_latih.std(axis=0).to_numpy()
X_gangguan = rng.normal(loc=rata2, scale=simpangan, size=(m, len(fitur)))

# 2) Bobot kemiripan: kernel Gaussian atas jarak Euclidean ternormalisasi
#    skala terhadap Budi; sampel jauh nyaris tak berpengaruh.
jarak = np.sqrt(((X_gangguan - nilai_budi) ** 2 / simpangan**2).sum(axis=1))
lebar_kernel = 0.75 * np.sqrt(len(fitur))
bobot = np.exp(-(jarak**2) / lebar_kernel**2)

# 3) Model linier lokal atas probabilitas forest, terbobot.
Z = (X_gangguan - rata2) / simpangan
X_gangguan_df = pd.DataFrame(X_gangguan, columns=fitur)  # nama fitur konsisten
proba_gangguan = forest.predict_proba(X_gangguan_df)[:, 1]
lokal = LinearRegression().fit(Z, proba_gangguan, sample_weight=bobot)

print("\nModel linier lokal di sekitar Budi (koefisien baku, terbobot):")
for nama, b in sorted(zip(fitur, lokal.coef_), key=lambda t: -abs(t[1])):
    print(f"  {nama:>14}: {b:+.3f}")
z_budi = (nilai_budi - rata2) / simpangan
prediksi_lokal = float(lokal.predict(z_budi.reshape(1, -1))[0])

r2_lokal = r2_score(proba_gangguan, lokal.predict(Z), sample_weight=bobot)
print(f"  intersep       : {lokal.intercept_:+.3f}")
print(f"\nPrediksi model lokal untuk Budi : {prediksi_lokal:.3f}")
print(f"Prediksi forest untuk Budi      : {proba_budi:.3f}")
print(f"R2 lokal terbobot (fidelity)    : {r2_lokal:.3f}")

Output yang Dihasilkan:

text
Pengajuan Budi (kasus paling batas di data uji):
      pendapatan =    3.58
     rasio_utang =    0.09
      lama_kerja =   17.81
            umur =   25.04
   riwayat_telat =    1.00
  prediksi forest = 0.490

Model linier lokal di sekitar Budi (koefisien baku, terbobot):
      pendapatan: +0.346
     rasio_utang: -0.195
      lama_kerja: +0.094
   riwayat_telat: -0.013
            umur: +0.009
  intersep       : +0.548

Prediksi model lokal untuk Budi : 0.494
Prediksi forest untuk Budi      : 0.490
R2 lokal terbobot (fidelity)    : 0.704

Tiga blok resep LIME terbaca langsung di kode. Baris 58–61 (gangguan): 300 sampel ditarik dari Normal per-fitur dengan parameter data latih — memutar profil semu di sekitar wilayah data. Baris 65–67 (bobot): jarak tiap sampel ke Budi dihitung ternormalisasi skala lalu dihaluskan kernel Gaussian; lebar_kernel = 0.75 * sqrt(jumlah fitur) mengikuti konvensi LIME. Baris 70–73 (model lokal): LinearRegression dilatih atas probabilitas forest dengan sample_weight=bobot, sehingga hanya tetangga dekat Budi yang menentukan koefisien.

Hasilnya menjelaskan keputusan yang memang paling sulit: probabilitas 0,490 — seserak mungkin dengan ambang. Budi berpendapatan Rp3,58 juta (rendah) tetapi nyaris tanpa cicilan (rasio 0,09) dan berkarier panjang (17,81 tahun); model linier lokal merangkum tarik-menariknya: pendapatan +0,346 dan lama kerja +0,094 mendorong naik, rasio utang −0,195 menahan, dua fitur derau di bawah 0,015. Fidelity lokalnya dua lapis: di titik Budi sendiri, model lokal memprediksi 0,494 lawan forest 0,490 — nyaris sempurna; di seluruh lingkungan terbobot, R² 0,704 — cukup baik untuk linearisasi atas 300 pohon. Batas implementasi juga harus disebut: sampel gangguan ditarik per-fitur secara independen sehingga kombinasi berkorelasi tidak terwakili, dan hasil LIME terkenal peka terhadap lebar kernel serta seed — dua kelemahan yang melekat pada idenya, bukan hanya pada implementasi manual ini.

Batas Interpretabilitas: Membaca Penyalinan dengan Jujur #

Empat batas menutup section ini. Pertama, fidelity adalah langit-langit kebenaran penjelasan: surrogate kedalaman 3 menyalin forest pada 0,880 prediksi uji, sehingga membaca dua belas baris aturannya sebagai “cara kerja forest” tetap mengklaim terlalu jauh — 15 baris perselisihan tidak punya juru bicara di aturan itu, dan baris-baris persisnya tidak diketahui pembaca aturan. Kedua, efek Rashomon: banyak penjelasan berbeda sama-sama setia — ganti random_state atau kedalaman, aturan yang tercetak berganti sementara fidelity nyaris tetap; penjelasan yang kita pilih adalah satu dari banyak kandidat, bukan satu-satunya kebenaran. Ketiga, penjelasan mendeskripsikan model, bukan dunia: seluruh alat post-hoc section ini — importance, PDP, ICE, SHAP, surrogate — memotret kebiasaan model pada data; ketika data berganti distribusi, potret itu kedaluwarsa bersama modelnya. Keempat, biaya kejujuran berulang: setiap klaim interpretasi perlu diverifikasi ulang (additivitas SHAP, fidelity surrogate, kestabilan antar seed) — pekerjaan yang tidak ada pada model intrinsik yang koefisiennya memang bagian dari struktur.

Kapan memilih jalur mana? Pedoman berikut merangkum section dalam satu tabel:

SituasiPilihan wajarAlasan
Eksplorasi data dan prototypingModel intrinsik (linier, pohon dangkal)Cepat, terbaca, jadi baseline gratis
Kotak hitam dipakai produksi, butuh auditSHAP lokal + pantauan fidelityPembukuan aditif per keputusan, terverifikasi
Regulator meminta aturan tertulisSurrogate global + laporan fidelityAturan dibaca manusia, batasnya diukur
Domain berisiko tinggi (kredit, kesehatan)Intrinsik dulu, kompleks hanya bila unggul nyataTaruhannya terlalu besar untuk penjelasan tak setia

Tidak ada yang salah dengan kotak hitam; yang salah adalah memakainya tanpa instrumen. Section ini telah melengkapi kita dengan empat instrumen — permutation importance untuk peringkat, PDP dan ICE untuk bentuk, SHAP untuk pembukuan per keputusan, surrogate untuk aturan — beserta cara menguji keterbatasan masing-masing. Satu batas terakhir tetap berdiri di luar jangkauan semuanya: seluruh instrumen tersebut menjelaskan asosiasi yang dipelajari model dari data observasional. Fitur penting bukan berarti fitur penyebab; kurva PDP yang menanjak bukan janji bahwa menaikkan variabel tersebut akan menaikkan keluaran di dunia nyata. Memisahkan asosiasi dari kausalitas membutuhkan kosakata dan alat yang sama sekali berbeda — dan itulah pintu masuk Section Causal Machine Learning, dimulai dari Chapter Korelasi vs Kausalitas.

Referensi Resmi #