Permutation importance di chapter sebelumnya berhasil memeringkat fitur: pendapatan dan rasio utang menopang skor kredit paling kuat. Tetapi daftar peringkat itu diam tentang satu hal yang justru paling sering ditanyakan tim bisnis: bentuk hubungannya seperti apa? Apakah menaikkan pendapatan selalu menaikkan peluang persetujuan dengan laju yang sama, atau efeknya jenuh setelah titik tertentu? Partial Dependence Plot (PDP) menjawab dengan menggambar kurva efek rata-rata satu fitur terhadap prediksi, dan saudaranya yang lebih jujur — Individual Conditional Expectation (ICE) — menggambar satu kurva per baris data sehingga perbedaan antar individu ikut terlihat. Kita tetap berpegang pada studi kasus Kredit Ceria, kali ini pada sisi penetapan plafon kredit (regresi), dengan mekanisme pembangkit yang memuat efek melengkung dan interaksi — dua hal yang tak tertangkap koefisien linier.
Mekanisme: Rata-rata Marginal atas Sebuah Grid #
PDP bekerja lewat eksperimen ganti-nilai yang sistematis. Untuk fitur yang ingin diperiksa, tentukan rentang grid (scikit-learn memakai persentil data, misalnya 40 titik antara persentil ke-0 dan ke-100). Di tiap titik grid, ganti nilai fitur tersebut pada semua baris dengan nilai grid, prediksi ulang seluruh baris, lalu rata-ratakan hasilnya. Kurva PDP adalah rangkaian rata-rata itu sepanjang grid. Fitur lain tidak dipegang tetap pada satu nilai — semuanya tetap membawa nilai asli tiap baris, sehingga efek yang tergambar adalah efek marginal rata-rata, bukan efek bersyarat pada satu profil.
ICE membuang langkah perata-rataan: kurva tiap baris disimpan apa adanya, menghasilkan selimut kurva yang tumpang-tindih. Ketika semua kurva ICE bergerak sejajar, PDP yang tunggal sudah mewakili semua orang; ketika kurva-kurva bersilangan atau berbeda kemiringannya, rata-rata PDP menyembunyikan heterogenitas — biasanya jejak interaksi antar-fitur. Kedua keluaran diperoleh dari satu panggilan yang sama: parameter kind="average" untuk PDP, kind="individual" untuk ICE, atau kind="both" untuk keduanya sekaligus.
Contoh: PDP dan ICE dengan Kebenaran yang Diketahui #
Mekanisme pembangkit data kita sengaja dibuat tiga lapis: efek pendapatan yang logaritmik (laju menurun), efek rasio utang yang kuadratik menurun, dan interaksi — pendapatan besar berdampak lebih kuat ketika rasio utang kecil. RandomForestRegressor 300 pohon dilatih memprediksi plafon, lalu partial_dependence dilesatkan pada pendapatan dengan kind="both". Karena kita yang menulis mekanismenya, PDP “kebenaran” bisa dihitung dengan rumus tertutup: rata-rata rasio utang dan lama kerja diganti nilai harapan matematisnya.
# PDP dan ICE pada regresi plafon kredit "Kredit Ceria" dengan ground truth
# yang diketahui: efek pendapatan logaritmik, efek rasio_utang kuadratik,
# dan interaksi pendapatan x rasio_utang. Nilai PDP model dibandingkan
# dengan PDP analitik (rumus tertutup), dan sebaran ICE mengukur
# heterogenitas akibat interaksi.
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.inspection import partial_dependence
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
# Mekanisme pembangkit: efek utama + interaksi pendapatan x rasio_utang.
# Pendapatan besar berdampak lebih kuat ketika rasio utang kecil.
plafon = (45.0
+ 14.0 * np.log(pendapatan / 3.0)
- 55.0 * np.square(rasio_utang)
+ 1.8 * np.sqrt(lama_kerja)
+ 6.0 * np.log(pendapatan / 3.0) * (1.0 - rasio_utang)
+ rng.normal(scale=2.0, size=n)) # juta rupiah
X = pd.DataFrame({
"pendapatan": pendapatan,
"rasio_utang": rasio_utang,
"lama_kerja": lama_kerja,
})
forest = RandomForestRegressor(
n_estimators=300, random_state=42, n_jobs=1).fit(X, plafon)
# PDP analitik (kebenaran) untuk pendapatan: rata-rata rasio_utang dan
# lama_kerja dihitung tertutup dari distribusi pembangkitnya.
E_RU, E_RU2, E_SQRT_LK = 0.5, 0.3175, 2.9814
def pdp_kebenaran_pendapatan(p):
return (45.0 + (14.0 + 6.0 * (1.0 - E_RU)) * np.log(p / 3.0)
- 55.0 * E_RU2 + 1.8 * E_SQRT_LK)
hasil_p = partial_dependence(
forest, X, features=["pendapatan"], kind="both", grid_resolution=40)
grid_p = hasil_p["grid_values"][0]
rata_p = hasil_p["average"][0]
ice_p = hasil_p["individual"][0] # bentuk (jumlah baris, jumlah grid)
indeks = np.unique(np.linspace(0, len(grid_p) - 1, 8).round().astype(int))
print("PDP pendapatan (efek rata-rata, juta rupiah) vs kebenaran analitik:")
print(f"{'pendapatan':>10} | {'PDP model':>9} | {'kebenaran':>9} | {'ICE min':>8} | {'ICE maks':>8} | {'lebar ICE':>9}")
for j in indeks:
lebar = ice_p[:, j].max() - ice_p[:, j].min()
print(f"{grid_p[j]:10.2f} | {rata_p[j]:9.2f} | {pdp_kebenaran_pendapatan(grid_p[j]):9.2f} | "
f"{ice_p[:, j].min():8.2f} | {ice_p[:, j].max():8.2f} | {lebar:9.2f}")
hasil_r = partial_dependence(
forest, X, features=["rasio_utang"], kind="average", grid_resolution=40)
grid_r = hasil_r["grid_values"][0]
rata_r = hasil_r["average"][0]
indeks_r = np.unique(np.linspace(0, len(grid_r) - 1, 6).round().astype(int))
print("\nPDP rasio_utang (efek rata-rata, juta rupiah):")
for j in indeks_r:
print(f" rasio_utang {grid_r[j]:.2f} -> plafon {rata_r[j]:.2f}")
kenaikan = ice_p[:, -1] - ice_p[:, 0] # perubahan plafon tiap pemohon, grid bawah -> atas
urut = np.argsort(kenaikan)
i_min, i_med, i_maks = urut[0], urut[len(urut) // 2], urut[-1]
print("\nKenaikan plafon dari grid bawah ke grid atas, per pemohon (ICE):")
print(f" terkecil : {kenaikan[i_min]:6.2f} juta (rasio_utang {rasio_utang[i_min]:.2f})")
print(f" median : {kenaikan[i_med]:6.2f} juta (rasio_utang {rasio_utang[i_med]:.2f})")
print(f" terbesar : {kenaikan[i_maks]:6.2f} juta (rasio_utang {rasio_utang[i_maks]:.2f})")
korelasi_delta = np.corrcoef(kenaikan, rasio_utang)[0, 1]
print(f"Korelasi kenaikan ICE vs rasio_utang: {korelasi_delta:.3f}")
print("PDP rata-rata hanya melaporkan satu kenaikan untuk semua pemohon;")
print("ICE memperlihatkan bahwa kenaikan itu berbeda-beda -- interaksi.")Output yang Dihasilkan:
PDP pendapatan (efek rata-rata, juta rupiah) vs kebenaran analitik:
pendapatan | PDP model | kebenaran | ICE min | ICE maks | lebar ICE
4.41 | 40.07 | 39.45 | 11.05 | 60.46 | 49.41
6.59 | 46.50 | 46.28 | 14.20 | 66.68 | 52.48
8.41 | 50.01 | 50.42 | 14.00 | 71.75 | 57.75
10.59 | 54.46 | 54.34 | 17.56 | 77.57 | 60.01
12.40 | 55.94 | 57.04 | 19.57 | 78.88 | 59.31
14.59 | 58.91 | 59.79 | 24.72 | 81.24 | 56.52
16.40 | 60.50 | 61.78 | 27.82 | 85.87 | 58.05
18.58 | 61.90 | 63.91 | 28.36 | 85.69 | 57.33
PDP rasio_utang (efek rata-rata, juta rupiah):
rasio_utang 0.09 -> plafon 70.54
rasio_utang 0.26 -> plafon 66.76
rasio_utang 0.43 -> plafon 57.62
rasio_utang 0.57 -> plafon 50.32
rasio_utang 0.74 -> plafon 37.47
rasio_utang 0.91 -> plafon 20.51
Kenaikan plafon dari grid bawah ke grid atas, per pemohon (ICE):
terkecil : 11.18 juta (rasio_utang 0.80)
median : 20.97 juta (rasio_utang 0.44)
terbesar : 33.15 juta (rasio_utang 0.05)
Korelasi kenaikan ICE vs rasio_utang: -0.788
PDP rata-rata hanya melaporkan satu kenaikan untuk semua pemohon;
ICE memperlihatkan bahwa kenaikan itu berbeda-beda -- interaksi.Empat bagian kode memikul tugas berbeda. Baris 21–26 menuliskan mekanisme pembangkit — perhatikan suku keempat, 6.0 * np.log(pendapatan/3.0) * (1.0 - rasio_utang), interaksi yang membuat efek pendapatan bergantung pada beban utang. Baris 38–41 menghitung PDP kebenaran secara analitik dengan nilai harapan tertutup (E_RU2 adalah E[rasio_utang²] distribusi seragam 0,05–0,95). Baris 43–47 memanggil partial_dependence dengan kind="both" dan grid_resolution=40; kamus hasilnya memuat grid_values (sumbu fitur), average (kurva PDP), dan individual (matriks ICE berbentuk jumlah-baris × jumlah-grid). Baris 67–75 merangkum tiap kurva ICE menjadi satu angka — kenaikan plafon dari grid bawah ke grid atas — lalu menghubungkannya dengan rasio utang masing-masing pemohon.
Tabel keluaran membaca seperti laporan pemeriksaan yang lulus. Kolom PDP model menempel rapat pada kolom kebenaran analitik (40,07 lawan 39,45 di grid bawah; 61,90 lawan 63,91 di grid atas) — bentuk logaritmik yang jenuh berhasil dipulihkan dari dalam pohon tanpa membuka satu simpul pun. PDP rasio utang memperlihatkan wajah kuadratiknya: plafon merosot dari 70,54 pada rasio 0,09 ke 20,51 pada rasio 0,91, dengan akselerasi di ujung tinggi. Baris-baris ICE lalu membongkar apa yang rata-rata tutupi: kenaikan plafon akibat pendapatan berkisar dari 11,18 juta (pemohon dengan rasio utang 0,80) sampai 33,15 juta (rasio utang 0,05), dengan korelasi −0,788 terhadap rasio utang. PDP tunggal akan melaporkan angka median sekitar 21 juta untuk semua orang — angka yang tidak menggambarkan siapa pun secara tepat. Di data seperti inilah membaca PDP tanpa ICE sama seperti membaca rata-rata penghasilan tanpa melihat sebarannya.
Bahaya Ekstrapolasi dan Kombinasi Tak Mungkin #
Kekuatan PDP — menilai satu fitur sambil membiarkan fitur lain bebas — sekaligus menjadi titik lemahnya saat fitur-fitur saling berkorelasi. Di dunia nyata, lama kerja dan umur bergerak seiring: pemohon berkarier 25 tahun hampir selalu berumur di atas 50. PDP lama kerja mengabaikan fakta itu — pada grid dua tahun, PDP menilai prediksi untuk seluruh baris termasuk pemohon berumur 58 tahun dengan karier dua tahun, kombinasi yang nyaris tidak pernah ada di data. Demo kedua mengukur akibatnya. Regresi Linier sengaja dipakai sebagai model karena koefisienya terpelajari bersih dan terpisah (bab Konsep Interpretabilitas), sehingga bias yang muncul murni bias alat, bukan bias model.
# Bahaya PDP pada fitur berkorelasi: umur dan lama_kerja bergerak seiring,
# tetapi PDP menilai lama_kerja pada seluruh nilai umur -- termasuk
# kombinasi yang tidak pernah terjadi di data. Regresi Linier dipakai agar
# kedua efek terpelajari secara terpisah dan bias PDP terlihat murni.
import numpy as np
import pandas as pd
from sklearn.inspection import partial_dependence
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(42)
n = 800
# Di data nyata, karier panjang dan usia lanjut datang bersama (korelasi tinggi).
umur = rng.uniform(25.0, 60.0, size=n) # tahun
lama_kerja = np.clip(0.8 * (umur - 23.0) + rng.normal(scale=4.0, size=n), 0.0, None)
plafon = (15.0 + 2.2 * lama_kerja + 0.35 * (umur - 25.0)
+ rng.normal(scale=2.0, size=n)) # juta rupiah
X = pd.DataFrame({"umur": umur, "lama_kerja": lama_kerja})
model = LinearRegression().fit(X, plafon)
korelasi = np.corrcoef(umur, lama_kerja)[0, 1]
print(f"Korelasi umur vs lama_kerja: {korelasi:.3f}")
print("Koefisien Regresi Linier (kedua efek terpelajari terpisah):")
print(f" lama_kerja : {model.coef_[1]:+.3f} juta per tahun kerja")
print(f" umur : {model.coef_[0]:+.3f} juta per tahun di atas 25")
hasil = partial_dependence(
model, X, features=["lama_kerja"], kind="average", grid_resolution=40)
grid = hasil["grid_values"][0]
rata = hasil["average"][0]
print(f"\nRentang umur data: {umur.min():.1f}-{umur.max():.1f}; "
f"rentang lama_kerja: {lama_kerja.min():.1f}-{lama_kerja.max():.1f}")
print(f"{'lk_grid':>7} | {'PDP':>7} | {'nyata':>7} | {'umur nyata':>10} | {'baris':>5}")
titik_grid = [2.0, 7.0, 12.0, 17.0, 22.0, 27.0]
pdp_titik, nyata_titik = [], []
for g in titik_grid:
pdp_g = float(np.interp(g, grid, rata))
dekat = np.abs(lama_kerja - g) <= 2.5
pred_nyata = model.predict(X[dekat]).mean()
print(f"{g:7.1f} | {pdp_g:7.2f} | {pred_nyata:7.2f} | {umur[dekat].mean():10.1f} | {int(dekat.sum()):5d}")
pdp_titik.append(pdp_g)
nyata_titik.append(pred_nyata)
kemiringan_pdp = np.polyfit(titik_grid, pdp_titik, 1)[0]
kemiringan_nyata = np.polyfit(titik_grid, nyata_titik, 1)[0]
print(f"\nKemiringan PDP : {kemiringan_pdp:.2f} juta per tahun kerja")
print(f"Kemiringan baris nyata : {kemiringan_nyata:.2f} juta per tahun kerja")
tak_mungkin = int(((lama_kerja <= 4.5) & (umur > 40.0)).sum())
print(f"\nBaris dengan lama_kerja <= 4.5 DAN umur > 40: {tak_mungkin} dari {n}")
print("PDP pada lk=2 tetap merata-ratakan prediksi atas seluruh umur 25-60;")
print("sebagian kombinasi yang dirata-ratakan tidak pernah terjadi di data.")Output yang Dihasilkan:
Korelasi umur vs lama_kerja: 0.899
Koefisien Regresi Linier (kedua efek terpelajari terpisah):
lama_kerja : +2.175 juta per tahun kerja
umur : +0.357 juta per tahun di atas 25
Rentang umur data: 25.0-60.0; rentang lama_kerja: 0.0-36.5
lk_grid | PDP | nyata | umur nyata | baris
2.0 | 25.67 | 20.22 | 29.0 | 113
7.0 | 36.55 | 33.51 | 32.6 | 118
12.0 | 47.43 | 45.75 | 38.0 | 144
17.0 | 58.30 | 59.25 | 44.6 | 134
22.0 | 69.18 | 72.04 | 50.2 | 144
27.0 | 80.06 | 83.98 | 54.2 | 107
Kemiringan PDP : 2.18 juta per tahun kerja
Kemiringan baris nyata : 2.56 juta per tahun kerja
Baris dengan lama_kerja <= 4.5 DAN umur > 40: 1 dari 800
PDP pada lk=2 tetap merata-ratakan prediksi atas seluruh umur 25-60;
sebagian kombinasi yang dirata-ratakan tidak pernah terjadi di data.Alur kodenya lurus: baris 17–19 membangun fitur berkorelasi 0,899 dengan target yang bergantung pada keduanya; baris 21 melatih Regresi Linier yang koefisienya tercetak di baris 26–27 (+2,175 dan +0,357 — keduanya dekat dengan kebenaran); baris 29–45 menghitung PDP lalu membandingkannya dengan rata-rata prediksi pada baris nyata di sekitar tiap titik grid (jendela ±2,5 tahun); baris 47–52 menutup dengan kemiringan kedua kurva dan penghitungan kombinasi yang tak pernah terjadi.
Angka biasnya sekarang bisa dibaca telak. Pada grid dua tahun, PDP melaporkan 25,67 juta sementara pemohon nyata berkarier pendek menerima 20,22 — selisih 5,45 juta murni karena PDP mencampurkan efek umur lanjut (yang di data nyata tidak pernah menyertai karier pendek). Di ujung lain, grid 27 tahun: PDP 80,06 lawan nyata 83,98, kini bias berbalik arah. Akibat kumulatifnya ada pada kemiringan: PDP 2,18 juta per tahun kerja versus 2,56 pada baris nyata — kurva PDP lebih landai karena sebagian efek yang di dunia datang bersama karier (kenaikan umur) tidak ikut bergerak saat partial_dependence mengganti nilai lama kerja. Dan bukti ekstrapolasinya tercetak di baris terakhir: dari 800 baris, hanya 1 yang memiliki karier di bawah 4,5 tahun sekaligus berumur di atas 40 — kombinasi yang PDP perlakukan seolah lazim.
Asosiasi, Bukan Kausalitas #
Satu batas terakhir sebelum melangkah. Kurva PDP yang menanjak mengatakan: menurut model, pemohon berpendapatan tinggi diprediksi menerima plafon lebih besar. Pernyataan itu berlaku untuk asosiasi — bila kita benar-benar menaikkan pendapatan seorang pemohon (misalnya lewat program kenaikan gaji), kenaikan plafon yang terjadi tidak harus mengikuti kurva tersebut, karena kurva digambar dari pola data observasional yang memuat faktor-faktor lain ikut bergerak. Seluruh alat interpretabilitas di section ini — importance, PDP, ICE, hingga SHAP yang akan kita temui sekarang — menjelaskan model, bukan menjelaskan dunia; pemisahan asosiasi dari kausalitas adalah pekerjaan Section Causal Machine Learning.
PDP dan ICE memeriksa satu fitur dalam satu waktu dan membutuhkan data banyak untuk rata-ratanya. Pertanyaan berikutnya lebih tajam lagi: untuk satu keputusan tunggal — pengajuan Budi ditolak hari Senin pukul 09.00 — berapa persisnya kontribusi tiap fitur, dengan jaminan aritmetika bahwa seluruh kontribusi berjumlah menjadi prediksi? Alat yang menjawab dengan fondasi teori permainan itu adalah Chapter SHAP Values.
Referensi Resmi #
- Partial dependence plots — scikit-learn User Guide — panduan resmi PDP dan ICE beserta diskusi contoh bergambar.
- partial_dependence — scikit-learn API — dokumentasi resmi fungsi yang dipakai kedua contoh.
- PartialDependenceDisplay — scikit-learn API
— pembungkus visual resmi untuk keluaran
partial_dependence.