PDP memberi kita bentuk rata-rata, ICE memberi sebaran per pemohon — tetapi keduanya tetap memeriksa satu fitur dalam satu waktu dan tidak pernah menjawab: untuk pengajuan ini, dari Budi yang ditolak hari Senin pagi, berapa persisnya jatah “salah” masing-masing fitur? SHAP values menjawab pertanyaan alokasi tersebut dengan fondasi yang tidak dimiliki alat lain: teori permainan kooperatif. Kontribusi tiap fitur dihitung dengan definisi keadilan matematis yang sama dipakai untuk membagi keuntungan antar pemain sejak Lloyd Shapley merumuskannya pada 1953 — sehingga hasilnya punya properti aritmetika yang bisa diverifikasi: nilai dasar ditambah seluruh kontribusi fitur selalu sama dengan prediksi model, tanpa sisa. Chapter ini membedah logika Shapley, memakai TreeExplainer untuk menghitungnya eksak pada RandomForest Kredit Ceria, lalu menaikkan penjelasan lokal itu menjadi ukuran kepentingan global.
Logika Nilai Shapley: Alokasi yang Adil #
Bayangkan prediksi model sebagai hasil kerja tim yang harus dibagi rata di antara para fitur. Satu cara naif: masukkan fitur satu per satu dan catat perubahan prediksi pada tiap kedatangan — tetapi urutan masuk menentukan besar kontribusi, dan fitur yang datang lebih dulu bisa mengambil kredit yang seharusnya milik yang belakangan. Nilai Shapley memutus sengketa urutan ini dengan cara yang tampak mahal namun adil: hitung kontribusi marginal setiap fitur di semua kemungkinan urutan masuk (atau ekuivalennya, semua koalisi fitur), lalu rata-ratakan. Fitur yang membantu di banyak koalisi mendapat porsi besar; fitur yang hanya tampak berguna pada urutan tertentu mendapat porsi yang sudah dipotong kelebihannya.
Dari definisi itu lahir dua properti yang membuat SHAP istimewa bagi praktisi. Pertama, efisiensi (aditivitas): jumlah seluruh nilai Shapley ditambah nilai dasar (base value) — keluaran model bila semua fitur “tidak hadir” — tepat sama dengan prediksi. Tidak ada kontribusi yang menggantung atau terhitung dua kali; pembukuan tertutup. Kedua, konsistensi: bila model direvisi sehingga suatu fitur makin berpengaruh, nilai Shapley fitur tersebut tidak akan turun. Brute force membutuhkan 2^p evaluasi model untuk p fitur — mustahil untuk ratusan fitur — tetapi untuk model berbasis pohon, algoritme TreeSHAP menghitung nilai yang sama secara eksak dalam waktu polinomial; itulah yang dijalankan shap.TreeExplainer.
Contoh 1: Penjelasan Lokal untuk Dua Pengajuan #
Kita kembali ke RandomForestClassifier 300 pohon pada data keputusan persetujuan Kredit Ceria. Dari 40 baris pertama data uji, dua pengajuan berkontras diambil — probabilitas tertinggi dan terendah — lalu diberi nama Budi dan Siti. TreeExplainer menghitung kontribusi lima fitur untuk masing-masing pengajuan dalam satuan probabilitas kelas disetujui.
# SHAP lokal: penjelasan per pengajuan pada RandomForest "Kredit Ceria".
# TreeExplainer menghitung nilai Shapley tiap fitur dalam satuan
# probabilitas kelas "disetujui", dan aditivitasnya diverifikasi numerik:
# nilai dasar + jumlah SHAP == prediksi model.
import numpy as np
import pandas as pd
import shap
from sklearn.ensemble import RandomForestClassifier
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)
# Dua pengajuan kontras dari data uji: probabilitas tertinggi dan
# terendah di antara 40 baris pertama.
proba_uji = forest.predict_proba(X_uji)[:, 1]
urut = np.argsort(proba_uji[:40])
X_contoh = X_uji.iloc[[int(urut[0]), int(urut[-1])]].copy()
X_contoh.index = ["Budi", "Siti"]
explainer = shap.TreeExplainer(forest)
shap_contoh = explainer.shap_values(X_contoh) # bentuk (2, 5, 2)
nilai_dasar = float(explainer.expected_value[1]) # probabilitas dasar
prediksi = forest.predict_proba(X_contoh)[:, 1]
print(f"Nilai dasar (base value) kelas 'disetujui': {nilai_dasar:.3f}")
for nama, i in [("Budi", 0), ("Siti", 1)]:
print(f"\nPengajuan {nama}: prediksi disetujui = {prediksi[i]:.3f}")
kontribusi = shap_contoh[i, :, 1]
pasangan = sorted(zip(fitur, X_contoh.iloc[i].to_numpy(), kontribusi),
key=lambda t: -abs(t[2]))
for f, nilai, s in pasangan:
arah = "menaikkan" if s > 0 else "menurunkan"
print(f" {f:>14} = {nilai:7.2f} SHAP {s:+.3f} ({arah})")
print(f" {'jumlah SHAP':>14} {kontribusi.sum():+.3f}")
selisih = float(np.abs(nilai_dasar + shap_contoh[:, :, 1].sum(axis=1) - prediksi).max())
print(f"\nVerifikasi aditivitas (nilai dasar + jumlah SHAP - prediksi):")
print(f" selisih maksimum = {selisih:.2e}")Output yang Dihasilkan:
Nilai dasar (base value) kelas 'disetujui': 0.507
Pengajuan Budi: prediksi disetujui = 0.010
pendapatan = 5.18 SHAP -0.298 (menurunkan)
rasio_utang = 0.87 SHAP -0.254 (menurunkan)
lama_kerja = 17.46 SHAP +0.047 (menaikkan)
umur = 48.08 SHAP +0.005 (menaikkan)
riwayat_telat = 1.00 SHAP +0.002 (menaikkan)
jumlah SHAP -0.497
Pengajuan Siti: prediksi disetujui = 0.997
rasio_utang = 0.34 SHAP +0.237 (menaikkan)
pendapatan = 10.00 SHAP +0.182 (menaikkan)
lama_kerja = 17.43 SHAP +0.054 (menaikkan)
umur = 34.87 SHAP +0.010 (menaikkan)
riwayat_telat = 1.00 SHAP +0.006 (menaikkan)
jumlah SHAP +0.489
Verifikasi aditivitas (nilai dasar + jumlah SHAP - prediksi):
selisih maksimum = 5.55e-16Empat bagian layak digarisbawahi. Baris 45–48 memilih dua baris uji yang berkontras lalu mengganti indeksnya menjadi nama — X_contoh.index = ["Budi", "Siti"] — murni penyajian; nilainya tetap baris data asli. Baris 50–53 membuat TreeExplainer dari forest dan memanggil shap_values(X_contoh); untuk classifier biner hasilnya berbentuk (jumlah baris, jumlah fitur, jumlah kelas), sehingga potongan [:, :, 1] mengambil kontribusi untuk kelas disetujui. Baris 52 membaca expected_value[1] — probabilitas dasar, yaitu keluaran rata-rata model atas data latih. Baris 66–68 adalah hakim chapter ini: selisih maksimum antara nilai_dasar + jumlah SHAP dan prediksi nyata model.
Keluarannya adalah narasi penolakan dan persetujuan yang bisa diaudit. Nilai dasar 0,507 adalah titik netral model sebelum satu pun fitur diperhitungkan. Pengajuan Budi mendarat di 0,010 — praktis ditolak — dan pembukuannya terbaca jelas: pendapatan Rp5,18 juta menarik −0,298 dan rasio utang 0,87 menarik −0,254; ketiga fitur lain nyaris tak berbicara; total −0,497, dan 0,507 − 0,497 = 0,010. Pengajuan Siti mendarat di 0,997 lewat jalan berlawanan: rasio utang rendah 0,34 menyumbang +0,237 dan pendapatan Rp10,00 juta menyumbang +0,182. Perhatikan pula detail yang sering luput: rasio utang Siti bukan sekadar “lebih kecil dari Budi” — pada kelas keputusan ini kontribusinya lebih besar daripada pendapatan, informasi yang permutation importance global tidak bisa berikan per kasus. Dan baris verifikasi menutup pembukuan: selisih maksimum 5,55e-16 — tingkat galat pembulatan floating point, bukan hampiran statistik.
Dari Lokal ke Global: Agregasi mean |SHAP| #
Kekuatan SHAP yang sesungguhnya muncul saat penjelasan lokal dikumpulkan. Rata-rata nilai mutlak SHAP per fitur atas seluruh data uji — np.abs(shap_values).mean(axis=0) — menjadi ukuran kepentingan global yang dihitung dari unit penjelasan lokal, bukan dari eksperimen pengacakan. Contoh kedua menghitung agregasi itu, membandingkan peringkatnya dengan permutation importance, lalu menambahkan arah efek lewat korelasi antara nilai fitur dan SHAP-nya sendiri.
# SHAP global: agregasi nilai Shapley seluruh data uji menjadi ukuran
# kepentingan fitur (mean |SHAP|), arah efek (korelasi SHAP vs nilai fitur),
# lalu perbandingan peringkatnya dengan permutation importance.
import numpy as np
import pandas as pd
import shap
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
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)
explainer = shap.TreeExplainer(forest)
shap_uji = explainer.shap_values(X_uji)[:, :, 1] # kontribusi kelas "disetujui"
mean_abs = np.abs(shap_uji).mean(axis=0)
urutan_shap = np.argsort(-mean_abs)
print(f"Agregasi mean |SHAP| atas {len(X_uji)} pengajuan data uji:")
for i in urutan_shap:
print(f" {fitur[i]:>14}: {mean_abs[i]:.3f}")
perm = permutation_importance(
forest, X_uji, y_uji, scoring="accuracy",
n_repeats=20, random_state=42, n_jobs=1)
urutan_perm = np.argsort(-perm.importances_mean)
print("\nPerbandingan peringkat (mean |SHAP| vs permutasi akurasi):")
for r in range(len(fitur)):
i, j = int(urutan_shap[r]), int(urutan_perm[r])
print(f" {r + 1}. SHAP: {fitur[i]:>14} ({mean_abs[i]:.3f}) "
f"permutasi: {fitur[j]:>14} ({perm.importances_mean[j]:+.3f})")
print("\nArah efek (korelasi nilai fitur vs nilai SHAP-nya):")
for j, nama in enumerate(fitur):
r = np.corrcoef(X_uji[nama].to_numpy(), shap_uji[:, j])[0, 1]
arah = "menaikkan" if r > 0 else "menurunkan"
print(f" {nama:>14}: {r:+.3f} -> nilai tinggi cenderung {arah} prediksi")Output yang Dihasilkan:
Agregasi mean |SHAP| atas 125 pengajuan data uji:
pendapatan: 0.241
rasio_utang: 0.207
lama_kerja: 0.058
umur: 0.013
riwayat_telat: 0.010
Perbandingan peringkat (mean |SHAP| vs permutasi akurasi):
1. SHAP: pendapatan (0.241) permutasi: pendapatan (+0.240)
2. SHAP: rasio_utang (0.207) permutasi: rasio_utang (+0.203)
3. SHAP: lama_kerja (0.058) permutasi: lama_kerja (+0.028)
4. SHAP: umur (0.013) permutasi: riwayat_telat (+0.003)
5. SHAP: riwayat_telat (0.010) permutasi: umur (-0.005)
Arah efek (korelasi nilai fitur vs nilai SHAP-nya):
pendapatan: +0.915 -> nilai tinggi cenderung menaikkan prediksi
rasio_utang: -0.949 -> nilai tinggi cenderung menurunkan prediksi
lama_kerja: +0.907 -> nilai tinggi cenderung menaikkan prediksi
umur: -0.297 -> nilai tinggi cenderung menurunkan prediksi
riwayat_telat: -0.642 -> nilai tinggi cenderung menurunkan prediksiTiga inti kodenya ringkas. Baris 43–44 menghitung SHAP seluruh 125 baris uji sekaligus lalu memotong kelas disetujui. Baris 46–47 melakukan agregasi np.abs(shap_uji).mean(axis=0) — rata-rata mutlak, karena kontribusi positif dan negatif sama-sama tanda fitur itu bekerja. Baris 64–68 mengorelasikan tiap kolom fitur dengan kolom SHAP-nya sendiri: korelasi positif berarti nilai tinggi cenderung menaikkan prediksi.
Kesepakatan dua alat itu meyakinkan pada bagian atas: pendapatan (0,241 lawan 0,240) dan rasio utang (0,207 lawan 0,203) bertukar tempat puncak dengan selisih nyaris nol, dan lama kerja duduk di posisi tiga pada keduanya. Ekor peringkat lalu saling bertukar urutan — umur dan riwayat keterlambatan bernilai 0,013 dan 0,010 versi SHAP, versus 0,003 dan −0,005 versi permutasi — dan itu wajar: kedua alat mengukur benda berbeda (SHAP mengukur pergerakan probabilitas rata-rata, permutasi menjatuhkan akurasi), dan pada fitur yang kontribusinya nyaris nol, urutan diisi derau sampling. Kolom arah efek menutup cerita dengan rapi: pendapatan (+0,915) dan lama kerja (+0,907) menaikkan prediksi, rasio utang (−0,949) dan riwayat keterlambatan (−0,642) menurunkan — tepat empat tanda yang kita tanam di mekanisme pembangkit — sementara umur (−0,297) memperlihatkan korelasi kecil yang tidak stabil: arah efek fitur tanpa informasi memang tidak bermakna, dan angkanya akan berganti-ganti bila seed diganti.
| Aspek | mean |SHAP| | Permutation importance |
|---|---|---|
| Satuan yang diukur | Pergerakan probabilitas prediksi | Jatuhnya skor (mis. akurasi) |
| Granularitas | Per baris, lalu diagregasi bebas | Hanya agregat |
| Ketergantungan model | TreeSHAP: eksak, cepat, khusus pohon | Agnostik, berlaku ke model apa pun |
| Pembukuan | Additif: base + Σ SHAP = prediksi | Tidak additif; hanya selisih skor |
| Biaya | Sekali hitung per data | n_repeats × jumlah fitur prediksi ulang |
SHAP telah memberi kita angka kontribusi per fitur per pengajuan, dengan pembukuan yang tertutup secara aritmetika. Tetapi perhatikan bentuk jawabannya: daftar bilangan. Manusia — khususnya tim risiko dan regulator — sering lebih nyaman dengan aturan: “tolak bila rasio utang di atas sekian dan pendapatan di bawa sekian”. Bisakah kotak hitam disalin menjadi aturan sepanjang itu, dan seberapa besar harga menyalinnya? Pertanyaan penutup section ini dijawab oleh Chapter Surrogate Model dan Batas Interpretabilitas.
Referensi Resmi #
- SHAP — dokumentasi resmi — panduan pustaka shap beserta konsep TreeSHAP.
- shap.TreeExplainer — API resmi — dokumentasi kelas yang dipakai kedua contoh.
- Permutation feature importance — scikit-learn User Guide — pembanding agregat yang dipakai contoh kedua.