Model yang bagus saat dilatih tidak menjamin model yang bagus setahun kemudian — bukan karena modelnya berubah, melainkan karena dunianya yang berubah. Pengunjung aplikasi bergeser demografisnya, sensor menua, tren musiman berganti arah. Pergeseran ini berjalan diam-diam: setiap prediksi tetap menghasilkan angka sah, tidak ada exception yang tercatat, dan layanan tampak sehat. Drift adalah nama untuk pergeseran ini, dan mendeteksinya adalah pekerjaan pengukuran distribusi — bukan pemeriksaan log error. Chapter ini mengimplementasikan PSI (Population Stability Index) dan KL divergence dengan NumPy murni, lalu membuktikan dampak drift yang sama terhadap akurasi model.

Tiga Istilah yang Sering Tertukar #

IstilahYang bergeserTerdeteksi dari
data drift (covariate shift)distribusi input P(X)membandingkan distribusi input masa kini vs baseline
concept drifthubungan input-label `P(yX)`
performance driftakurasi/metrik modellabel terlambat tiba, lalu metrik dihitung ulang

Ketiganya bisa terjadi sendiri-sendiri atau bersamaan. Data drift adalah sinyal paling dini karena bisa diukur tanpa label baru — cukup membandingkan distribusi input terhadap baseline saat training. Itulah sebabnya loop monitoring di chapter Monitoring Model Produksi memantau distribusi prediksi: sinyal tanpa label yang menunjukkan sesuatu sedang berubah.

PSI: Mengukur Pergeseran Distribusi #

PSI membandingkan dua distribusi lewat histogram dengan tepi bin yang sama. Untuk proporsi per-bin baseline b_i dan arus terbaru a_i:

text
PSI = Σ (a_i − b_i) · ln(a_i / b_i)

Rumus sederhana ini memiliki dua sifat yang berguna: nilainya selalu tidak-negatif (nol hanya jika kedua distribusi identik per bin), dan arah pergeseran tidak memengaruhi besarnya — bergeser ke kanan atau ke kiri menghasilkan angka besar yang sama. Konvensi ambang industri yang lazim: di bawah 0.10 stabil, 0.10-0.25 perlu perhatian, di atas 0.25 bergeser signifikan. KL divergence KL = Σ a_i · ln(a_i / b_i) menakar hal serupa dengan penekanan relatif, tetapi asimetris: KL(aktual‖baseline) tidak sama dengan kebalikannya.

Contoh Teruji: PSI dan KL dengan NumPy #

Berkas psi_drift.py membangun baseline 5000 sampel, lalu membandingkannya dengan tiga arus data: stabil, bergeser sedang, dan bergeser jauh. Segmen kedua menunjukkan pergeseran yang sama terlihat dari sisi model:

python
"""Deteksi data drift dengan PSI dan KL divergence (NumPy murni).

Baseline distribusi dibandingkan dengan tiga arus data terbaru.
PSI dan KL dihitung dari histogram dengan tepi bin tetap, lalu
dibandingkan terhadap ambang industri: PSI < 0.10 stabil,
0.10-0.25 waspada, > 0.25 bergeser signifikan. Segmen terakhir
menunjukkan drift yang sama terlihat sebagai penurunan akurasi
model (performance drift).
"""

import numpy as np

TEPI_BIN = np.array([-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0])
AMBANG_WASPADA = 0.10
AMBANG_DRIFT = 0.25


def proporsi_bin(sampel):
    """Proporsi sampel per bin; tepi bin menampung nilai ekstrem."""
    terpotong = np.clip(sampel, TEPI_BIN[0], TEPI_BIN[-1])
    hitungan, _ = np.histogram(terpotong, bins=TEPI_BIN)
    return (hitungan + 0.5) / hitungan.sum()  # smoothing anti nol


def psi(baseline, aktual):
    """Population Stability Index antara dua sampel."""
    pb = proporsi_bin(baseline)
    pa = proporsi_bin(aktual)
    return float(np.sum((pa - pb) * np.log(pa / pb)))


def kl_divergence(baseline, aktual):
    """KL(aktual || baseline) dari histogram yang sama."""
    pb = proporsi_bin(baseline)
    pa = proporsi_bin(aktual)
    return float(np.sum(pa * np.log(pa / pb)))


def status_psi(nilai):
    if nilai < AMBANG_WASPADA:
        return "stabil"
    if nilai < AMBANG_DRIFT:
        return "waspada"
    return "DRIFT SIGNIFIKAN"


def main():
    rng = np.random.default_rng(42)
    baseline = rng.normal(0.0, 1.0, size=5000)

    arus = {
        "arus stabil": np.random.default_rng(1).normal(0.1, 1.0, 5000),
        "arus bergeser sedang": np.random.default_rng(2).normal(
            0.4, 1.0, 5000),
        "arus bergeser jauh": np.random.default_rng(3).normal(
            1.2, 1.5, 5000),
    }

    print("== PSI dan KL: baseline vs arus data terbaru ==")
    print(f"{'kondisi':<22} {'PSI':>7} {'KL':>7}  status")
    for nama, sampel in arus.items():
        p = psi(baseline, sampel)
        k = kl_divergence(baseline, sampel)
        print(f"{nama:<22} {p:>7.4f} {k:>7.4f}  {status_psi(p)}")

    p_jauh = psi(baseline, arus["arus bergeser jauh"])
    if p_jauh > AMBANG_DRIFT:
        print(f"\nALERT: PSI {p_jauh:.4f} > {AMBANG_DRIFT} -> "
              "periksa sumber data dan siapkan retraining")

    print("\n== drift yang sama, dilihat dari performa model ==")
    rng_m = np.random.default_rng(9)
    X_latih = np.vstack([
        rng_m.normal([-1.0, 0.0], 0.7, size=(150, 2)),
        rng_m.normal([1.0, 0.4], 0.7, size=(150, 2)),
    ])
    y_latih = np.array([0] * 150 + [1] * 150)
    w = np.zeros(3)
    for _ in range(300):
        skor = np.clip(X_latih @ w[1:] + w[0], -30, 30)
        p = 1.0 / (1.0 + np.exp(-skor))
        galat = p - y_latih
        w[1:] -= (X_latih.T @ galat) / 300
        w[0] -= galat.mean()

    rng_uji = np.random.default_rng(5)
    X_uji_normal = np.vstack([
        rng_uji.normal([-1.0, 0.0], 0.7, size=(100, 2)),
        rng_uji.normal([1.0, 0.4], 0.7, size=(100, 2)),
    ])
    y_uji = np.array([0] * 100 + [1] * 100)
    X_uji_geser = X_uji_normal + np.array([1.5, 0.0])  # dunia bergeser

    ak_normal = float((((X_uji_normal @ w[1:] + w[0] >= 0).astype(int))
                       == y_uji).mean())
    ak_geser = float((((X_uji_geser @ w[1:] + w[0] >= 0).astype(int))
                      == y_uji).mean())
    print(f"akurasi pada data seperti latih : {ak_normal:.3f}")
    print(f"akurasi pada data bergeser      : {ak_geser:.3f}")
    print("model tidak berubah — dunialah yang berubah")


if __name__ == "__main__":
    main()

Output yang Dihasilkan:

text
== PSI dan KL: baseline vs arus data terbaru ==
kondisi                    PSI      KL  status
arus stabil             0.0106  0.0053  stabil
arus bergeser sedang    0.1664  0.0833  waspada
arus bergeser jauh      1.1258  0.6932  DRIFT SIGNIFIKAN

ALERT: PSI 1.1258 > 0.25 -> periksa sumber data dan siapkan retraining

== drift yang sama, dilihat dari performa model ==
akurasi pada data seperti latih : 0.945
akurasi pada data bergeser      : 0.625
model tidak berubah — dunialah yang berubah

Mari kita telaah kode di atas baris per baris:

  1. Baris 18-22 (proporsi_bin): sampel dipotong ke rentang tepi bin dengan np.clip — nilai ekstrem di luar -3..3 menumpuk di bin tepi, tidak dibuang, sehingga ekor panjang tetap terhitung. Smoothing +0.5 menjaga proporsi tidak pernah nol; tanpa ini, satu bin kosong membuat logaritma dan pembagian menjadi tak terdefinisi.
  2. Baris 25-29 (psi): implementasi rumus PSI dalam satu baris NumPy. Tidak ada pustaka tambahan — begitulah cara meyakinkan diri bahwa metrik monitoring bukan kotak hitam.
  3. Baris 32-35 (kl_divergence): KL dari histogram yang sama. Perhatikan pada keluaran: PSI arus stabil 0.0106 vs KL 0.0053 — keduanya searah, PSI kira-kira dua kali KL untuk pergeseran kecil; pada arus jauh, PSI 1.1258 vs KL 0.6932. Konvensi ambang yang populer memakai PSI karena interpretasinya lebih stabil.
  4. Baris 51-57 (tiga arus): perbedaan ketiga arus hanya pada parameter distribusi — geser rata-rata 0.1, 0.4, dan 1.2 dengan simpangan baku 1.5 pada yang terakhir. Hasilnya jatuh tepat ke tiga pita ambang: 0.0106 stabil, 0.1664 waspada, 1.1258 alarm.
  5. Baris 66-69 (alarm): alarm tercetak menyebutkan nilai PSI, ambang, dan tindakan — sama seperti disiplin alert di chapter Monitoring Model Produksi .
  6. Baris 71-99 (performance drift): model dilatih pada data normal, lalu diuji pada data yang digeser +1.5 pada fitur pertama. Akurasi runtuh dari 0.945 ke 0.625 tanpa satu baris kode model berubah — kalimat penutup skrip merangkum semuanya: model tidak berubah, dunialah yang berubah.

Dari Deteksi ke Tindakan #

PSI menjawab “apakah bergeser”; keputusan berikutnya adalah menentukan respons. Pilihan umum dirangkum tabel berikut:

KondisiTindakanCatatan
PSI < 0.10tidak adacatat nilai untuk tren
PSI 0.10-0.25investigasi sumber dataperiksa pipeline input, segmen pengguna baru
PSI > 0.25retraining dengan data segardata drift permanen menuntut data latih yang mewakili
PSI stabil tapi akurasi turuncuriga concept driftperlu label baru; bandingkan error per segmen

Drift menjelaskan mengapa performa bisa merosot; pekerjaan berikutnya adalah memantau performa itu sendiri secara berkelanjutan — termasuk menghadapi kenyataan bahwa label kebenaran tidak pernah tersedia seketika. Chapter berikutnya, Monitoring Kinerja Model , membangun monitor akurasi jendela waktu dengan label terlambat.