Chapter Fairness dan Bias Algoritmik berakhir pada satu kesimpulan yang tidak nyaman: memilih definisi fairness adalah keputusan kebijakan, dan keputusan kebijakan menuntut bukti — angka yang terdokumentasi, kejadian yang tercatat, dan orang yang bisa dimintai tanggung jawab. Chapter penutup section ini merangkai praktik machine learning yang bertanggung jawab menjadi satu alur kerja utuh: dokumentasi berupa kartu model yang dibangun dari artefak, jejak audit yang bisa diverifikasi, monitoring yang sudah kita bangun di Section Deployment dan Operasional, respons insiden yang terdokumentasi, dan reviu manusia sebagai gerbang terakhir sebelum model menyentuh keputusan nyata. Dua praktikum menutup section: kartu model yang dihasilkan otomatis sebagai JSON, dan jejak audit berantai hash yang mendeteksi insiden drift lalu menahan rilis versi berikutnya. Seluruh tekniknya sederhana — dan justru itu intinya: praktik bertanggung jawab bukan algoritma khusus, melainkan disiplin yang bisa dijalankan sejak proyek pertama.

Peta Praktik: Lima Pilar yang Saling Mengunci #

Praktik bertanggung jawab bisa diringkas menjadi satu kalimat operasional: setiap keputusan penting harus bisa ditelusuri, ditinjau, dan dibatalkan. Ditelusuri berarti ada jejak yang menunjukkan data, kode, dan orang di balik tiap versi model. Ditinjau berarti ada dokumentasi yang cukup bagi orang di luar tim pembangun untuk menilai batasan model. Dibatalkan berarti ada jalur yang teruji untuk menahan atau mengembalikan model ketika perilakunya menyimpang. Lima pilar berikut mewujudkan ketiganya.

PilarArtefak konkretBersumber dari
DokumentasiKartu model berisi data, metrik, batasanChapter Fairness dan Bias Algoritmik untuk metrik per kelompok
Jejak auditLog kejadian berantai yang bisa diverifikasiSeluruh siklus hidup model
MonitoringMetrik terpantau + pemicu eskalasiChapter Monitoring dan Deteksi Drift
Respons insidenRunbook + keputusan yang tercatatChapter Pembaruan Model dan Retraining
Reviu manusiaGerbang persetujuan sebelum rilisSeluruh sistem berdampak pada individu

Kelima pilar saling mengunci: monitoring tanpa respons insiden hanya menghasilkan laporan yang tidak pernah ditindaklanjuti; jejak audit tanpa reviu manusia mencatat kejadian tanpa pertanggungjawaban; kartu model tanpa monitoring menjadi dokumen yang diam padahal dunia bergerak.

Kartu Model yang Dibangun dari Artefak #

Masalah nyatanya: dokumentasi yang ditulis tangan hampir selalu kedaluwarsa diam-diam — model dilatih ulang, metrik berubah, tetapi berkas dokumentasi menyimpan angka lama. Mekanisme jawabannya membalik arah kerja: kartu model (model card) tidak ditulis, melainkan dihitung. Satu eksekusi yang sama melatih model, mengevaluasinya, lalu merangkum hasilnya menjadi data terstruktur — JSON — sehingga versi kartu dan versi model tidak mungkin berbeda. Format kartu model sendiri diperkenalkan komunitas machine learning pada 2019; bagi kita, isinya lebih penting daripada formalismenya.

Praktikum pertama membangun kartu untuk penapis kredit mikro: 600 pengajuan dengan tiga fitur, target pinjaman lancar atau macet. Keputusan minimisasi data dari Chapter Privasi Data dan Machine Learning ikut tercatat — kolom identitas dan atribut demografis memang tidak dikumpulkan, dan kartu menuliskan konsekuensinya secara eksplisit.

python
# Kartu model (model card) yang dibangun dari artefak nyata, bukan ditulis
# tangan: setiap angka pada kartu dihitung ulang dari data, model, dan
# evaluasi pada saat kartu dibuat, lalu dikeluarkan sebagai data terstruktur
# (JSON). Dengan begitu kartu tidak bisa kedaluwarsa diam-diam - angka kartu
# dan perilaku model selalu lahir dari eksekusi yang sama.
# Skenario: penapis pengajuan kredit mikro untuk prediksi pinjaman lancar
# atau macet. Keputusan minimisasi data dicatat di dalam kartu: kolom
# identitas dan alamat tidak dikumpulkan sejak awal.

import json

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

# ---------- Data sintetis: 600 pengajuan kredit mikro ----------
# Kemampuan bayar laten menggerakkan seluruh fitur dan label.
rng = np.random.default_rng(42)
n = 600

kemampuan_bayar = rng.normal(0, 1, size=n)
penghasilan_juta = 9 + 2.0 * kemampuan_bayar + rng.normal(0, 1.0, size=n)
masa_kerja_tahun = 6 + 1.5 * kemampuan_bayar + rng.normal(0, 2.0, size=n)
rasio_utang = 0.35 - 0.10 * kemampuan_bayar + rng.normal(0, 0.05, size=n)

# Label: pinjaman lancar satu tahun kemudian (dengan derau keputusan).
lancar = (kemampuan_bayar + rng.normal(0, 0.75, size=n) > 0).astype(int)

data = pd.DataFrame({
    "penghasilan_juta": penghasilan_juta,
    "masa_kerja_tahun": masa_kerja_tahun,
    "rasio_utang": rasio_utang,
    "lancar": lancar,
})

# Minimisasi data (chapter privasi): nomor identitas, alamat, dan atribut
# demografis tidak dikumpulkan - hanya fitur yang benar-benar dipakai model.
fitur = ["penghasilan_juta", "masa_kerja_tahun", "rasio_utang"]
X_latih, X_uji, y_latih, y_uji = train_test_split(
    data[fitur], data["lancar"], test_size=0.35,
    stratify=data["lancar"], random_state=42
)

model = LogisticRegression(max_iter=1000).fit(X_latih, y_latih)
prediksi = model.predict(X_uji)


# ---------- Metrik uji dihitung dari artefak yang sama ----------
def tabel_kontingensi(p, t):
    """TP, FP, TN, FN dari prediksi biner dan label."""
    return {
        "TP": int(((p == 1) & (t == 1)).sum()),
        "FP": int(((p == 1) & (t == 0)).sum()),
        "TN": int(((p == 0) & (t == 0)).sum()),
        "FN": int(((p == 0) & (t == 1)).sum()),
    }


k = tabel_kontingensi(prediksi, y_uji.values)
akurasi = float((k["TP"] + k["TN"]) / sum(k.values()))
tpr = float(k["TP"] / (k["TP"] + k["FN"]))
fpr = float(k["FP"] / (k["FP"] + k["TN"]))

# ---------- Kartu model sebagai data terstruktur ----------
kartu = {
    "identitas": {
        "nama": "penapis-kredit-mikro",
        "versi": "1.0",
        "tugas": "klasifikasi biner (lancar / macet)",
        "pemilik": "tim-data",
        "status": "kandidat rilis - menunggu reviu manusia",
    },
    "data_latih": {
        "jumlah_baris": int(len(X_latih)),
        "fitur": fitur,
        "kolom_tidak_dikumpulkan": [
            "nomor_identitas", "alamat", "atribut_demografis"
        ],
        "alasan": "minimisasi data - tidak dibutuhkan untuk prediksi",
        "proporsi_kelas_lancar": round(float(y_latih.mean()), 3),
    },
    "pelatihan": {
        "algoritma": "LogisticRegression",
        "hyperparameter": {
            nama: model.get_params()[nama]
            for nama in ("penalty", "C", "solver", "max_iter")
        },
    },
    "metrik_uji": {
        "akurasi": round(akurasi, 3),
        "tpr": round(tpr, 3),
        "fpr": round(fpr, 3),
    },
    "pertimbangan_fairness": (
        "atribut kelompok tidak dikumpulkan, sehingga disparitas antar "
        "kelompok tidak bisa diaudit pada sistem ini; sistem yang "
        "keputusannya menyentuh kelompok demografis wajib menambahkan "
        "bagian metrik per kelompok"
    ),
    "batasan": [
        "data sintetis untuk latihan, bukan distribusi kredit nyata",
        "evaluasi holdout tunggal, tidak menggantikan pemantauan berkala",
        "validitas untuk penghasilan di luar rentang data latih tidak diuji",
    ],
    "rencana_pemantauan": {
        "metrik_dipantau": ["akurasi", "tpr", "fpr"],
        "pemicu_eskalasi": "pergeseran distribusi fitur melewati ambang",
        "jawaban_pemicu": "tahan promosi versi baru, buka reviu insiden",
    },
    "persetujuan": {
        "peninjau": "Dewi",
        "keputusan": "belum ada - gerbang manusia belum dilewati",
    },
}

print("== Kartu model penapis-kredit-mikro v1.0 (dibangun dari artefak) ==")
print(json.dumps(kartu, indent=2, ensure_ascii=False))
print()
print("kartu dihasilkan satu eksekusi dengan model; setiap angka dihitung")
print("ulang saat kartu dibuat sehingga versi kartu dan versi model tidak")
print("bisa berbeda diam-diam")

Output yang Dihasilkan:

text
== Kartu model penapis-kredit-mikro v1.0 (dibangun dari artefak) ==
{
  "identitas": {
    "nama": "penapis-kredit-mikro",
    "versi": "1.0",
    "tugas": "klasifikasi biner (lancar / macet)",
    "pemilik": "tim-data",
    "status": "kandidat rilis - menunggu reviu manusia"
  },
  "data_latih": {
    "jumlah_baris": 390,
    "fitur": [
      "penghasilan_juta",
      "masa_kerja_tahun",
      "rasio_utang"
    ],
    "kolom_tidak_dikumpulkan": [
      "nomor_identitas",
      "alamat",
      "atribut_demografis"
    ],
    "alasan": "minimisasi data - tidak dibutuhkan untuk prediksi",
    "proporsi_kelas_lancar": 0.5
  },
  "pelatihan": {
    "algoritma": "LogisticRegression",
    "hyperparameter": {
      "penalty": "l2",
      "C": 1.0,
      "solver": "lbfgs",
      "max_iter": 1000
    }
  },
  "metrik_uji": {
    "akurasi": 0.757,
    "tpr": 0.752,
    "fpr": 0.238
  },
  "pertimbangan_fairness": "atribut kelompok tidak dikumpulkan, sehingga disparitas antar kelompok tidak bisa diaudit pada sistem ini; sistem yang keputusannya menyentuh kelompok demografis wajib menambahkan bagian metrik per kelompok",
  "batasan": [
    "data sintetis untuk latihan, bukan distribusi kredit nyata",
    "evaluasi holdout tunggal, tidak menggantikan pemantauan berkala",
    "validitas untuk penghasilan di luar rentang data latih tidak diuji"
  ],
  "rencana_pemantauan": {
    "metrik_dipantau": [
      "akurasi",
      "tpr",
      "fpr"
    ],
    "pemicu_eskalasi": "pergeseran distribusi fitur melewati ambang",
    "jawaban_pemicu": "tahan promosi versi baru, buka reviu insiden"
  },
  "persetujuan": {
    "peninjau": "Dewi",
    "keputusan": "belum ada - gerbang manusia belum dilewati"
  }
}

kartu dihasilkan satu eksekusi dengan model; setiap angka dihitung
ulang saat kartu dibuat sehingga versi kartu dan versi model tidak
bisa berbeda diam-diam

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 19–38 (data dan minimisasi): rng berseed 42 membangkitkan satu variabel laten kemampuan_bayar yang menggerakkan ketiga fitur dan label — pola yang sama dengan chapter fairness, sehingga metrik pada kartu bisa dibandingkan lintas praktikum. Kolom identitas dan atribut demografis tidak pernah masuk DataFrame — keputusan minimisasi diterapkan sebelum data ada, bukan dibuang setelahnya.
  2. Baris 50–63 (metrik dari artefak): fungsi tabel_kontingensi menghitung TP/FP/TN/FN persis seperti chapter fairness, lalu akurasi, tpr, dan fpr menjadi isi bagian metrik_uji — kartu dan evaluasi berbagi sumber angka yang sama.
  3. Baris 66–82 (identitas dan data latih): jumlah_baris dibaca dari len(X_latih), kolom_tidak_dikumpulkan mencatat keputusan privasi, dan proporsi_kelas_lancar dihitung dari y_latih — tidak ada satu angka pun yang diketik tangan.
  4. Baris 83–88 (hyperparameter): nilai penalty, C, solver, dan max_iter diambil langsung dari model.get_params() — bila versi berikutnya mengubah C, kartu berubah pada eksekusi yang sama.
  5. Baris 95–100 (pertimbangan_fairness): bagian ini jujur menyatakan bahwa disparitas antar kelompok tidak bisa diaudit karena atributnya tidak dikumpulkan — pertukaran nyata antara minimisasi dan auditability yang sebaiknya diputuskan sadar, bukan secara kebetulan.

Cara membaca keluarannya: kartu memuat akurasi 0,757, TPR 0,752, dan FPR 0,238 pada data uji — angka yang wajar untuk keputusan kredit — tetapi bagian paling pentingnya justru batasan dan persetujuan. Status “kandidat rilis - menunggu reviu manusia” menandakan model ini belum boleh dipromosikan; peninjau Dewi belum memberi keputusan. Kartu yang baik tidak menjual model, kartu yang baik mengungkapkan kondisinya.

Jejak Audit: Rantai Kejadian yang Bisa Diverifikasi #

Masalah kedua: klaim seperti “model sudah direviu” atau “insiden sudah ditangani” tidak berharga tanpa bukti yang bisa diperiksa ulang. Mekanismenya: jejak audit (audit trail) berbentuk daftar kejadian yang hanya bisa ditambah — data masuk, pelatihan, pembuatan kartu, reviu, insiden, respons — dan setiap entri menyimpan hash yang dihitung dari isi entri itu ditambah hash entri sebelumnya. Rantai semacam ini membuat jejak tamper-evident: mengubah satu entri lama mengubah hash-nya, dan karena hash itu ikut dihitung pada entri berikutnya, seluruh rantai setelahnya ikut berubah — penyimpangan pasti terdeteksi saat verifikasi.

Praktikum kedua menjalankan siklus hidup lengkap pada skenario kartu kredit yang sama, lalu mensimulasikan kejadian produksi: monitoring membandingkan batch baru terhadap data referensi, menemukan rasio_utang bergeser 3,60 simpangan baku, dan respons insiden tercatat sebagai entri jejak — lengkap dengan keputusan kembali ke keputusan manual dan jadwal retraining.

python
# Jejak audit (audit trail) sebagai rantai hash, plus simulasi insiden
# drift beserta responsnya yang terdokumentasi. Setiap kejadian pada
# siklus hidup model dicatat sebagai satu entri; hash entri mencakup hash
# entri sebelumnya, sehingga urutan dan isi jejak bisa diverifikasi
# menyeluruh - mengubah satu entri lama secara diam-diam akan membatalkan
# seluruh hash setelahnya. Alur lengkapnya: data masuk, pelatihan,
# pembuatan kartu model, reviu manusia, lalu monitoring mendeteksi
# pergeseran distribusi pada batch produksi dan respons insiden dicatat.

import hashlib
import json

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

# ---------- Data dan model: skenario yang sama dengan kartu-model.py ----------
rng = np.random.default_rng(42)
n = 600

kemampuan_bayar = rng.normal(0, 1, size=n)
penghasilan_juta = 9 + 2.0 * kemampuan_bayar + rng.normal(0, 1.0, size=n)
masa_kerja_tahun = 6 + 1.5 * kemampuan_bayar + rng.normal(0, 2.0, size=n)
rasio_utang = 0.35 - 0.10 * kemampuan_bayar + rng.normal(0, 0.05, size=n)
lancar = (kemampuan_bayar + rng.normal(0, 0.75, size=n) > 0).astype(int)

data = pd.DataFrame({
    "penghasilan_juta": penghasilan_juta,
    "masa_kerja_tahun": masa_kerja_tahun,
    "rasio_utang": rasio_utang,
    "lancar": lancar,
})
fitur = ["penghasilan_juta", "masa_kerja_tahun", "rasio_utang"]

X_latih, X_uji, y_latih, y_uji = train_test_split(
    data[fitur], data["lancar"], test_size=0.35,
    stratify=data["lancar"], random_state=42
)
model = LogisticRegression(max_iter=1000).fit(X_latih, y_latih)
akurasi_uji = float((model.predict(X_uji) == y_uji).mean())

# Sidik jari dataset: hash atas isi data - identik bila identik byte.
hash_dataset = hashlib.sha256(
    data.to_csv(index=False).encode()
).hexdigest()[:12]

# ---------- Jejak audit: entri berantai lewat hash ----------
jejak = []


def catat(kejadian, aktor, detail):
    """Tambah satu entri; hash-nya mengikat entri sebelumnya."""
    entri = {
        "seq": len(jejak) + 1,
        "kejadian": kejadian,
        "aktor": aktor,
        "detail": detail,
    }
    sebelumnya = jejak[-1]["hash"] if jejak else "0" * 12
    inti = json.dumps(
        {k: entri[k] for k in ("seq", "kejadian", "aktor", "detail")},
        sort_keys=True, ensure_ascii=False,
    )
    entri["hash"] = hashlib.sha256(
        (sebelumnya + inti).encode()
    ).hexdigest()[:12]
    jejak.append(entri)


catat("data-masuk", "pipeline-data", {"baris": n, "hash_dataset": hash_dataset})
catat("latih-model", "pipeline-latih",
      {"algoritma": "LogisticRegression", "akurasi_uji": round(akurasi_uji, 3)})
catat("kartu-model-dibuat", "sistem", {"versi": "1.0"})
catat("reviu-manusia", "Dewi",
      {"keputusan": "setuju-dirilis", "catatan": "metrik dapat diterima"})

# ---------- Monitoring: batch produksi baru diperiksa terhadap referensi ----------
# Batch produksi meniru populasi data latih, tetapi rasio_utang bergeser
# naik 0.40 (skenario tekanan ekonomi). z = selisih rerata dibagi simpangan
# baku referensi, dibandingkan terhadap ambang eskalasi 3.0.
batch = data[fitur].copy()
batch["rasio_utang"] = batch["rasio_utang"] + 0.40
AMBANG_Z = 3.0

print("== Monitoring batch produksi terhadap data referensi ==")
print("fitur             |     z | status")
pemicu = None
for f in fitur:
    z = float(abs(batch[f].mean() - data[f].mean()) / data[f].std())
    status = "eskalasi" if z > AMBANG_Z else "stabil"
    if z > AMBANG_Z and pemicu is None:
        pemicu = (f, z)
    print(f"{f:17s} | {z:5.2f} | {status}")

# ---------- Respons insiden tercatat sebagai entri jejak ----------
if pemicu is not None:
    fitur_pemicu, z_pemicu = pemicu
    catat("insiden-drift", "monitoring",
          {"fitur": fitur_pemicu, "z": round(z_pemicu, 2), "ambang": AMBANG_Z})
    catat("respons-insiden", "Siti",
          {"aksi": "kembali ke keputusan manual",
           "tindak_lanjut": "retraining lalu reviu ulang kartu model"})

# ---------- Cetak jejak lengkap ----------
print()
print("== Jejak audit penapis-kredit-mikro (rantai hash) ==")
for e in jejak:
    detail = json.dumps(e["detail"], ensure_ascii=False, sort_keys=True)
    print(f"{e['seq']:2d} | {e['hash']} | {e['kejadian']:18s} | "
          f"{e['aktor']:14s} | {detail}")

# ---------- Verifikasi integritas: hitung ulang seluruh rantai ----------
def verifikasi(daftar):
    sebelumnya = "0" * 12
    for entri in daftar:
        inti = json.dumps(
            {k: entri[k] for k in ("seq", "kejadian", "aktor", "detail")},
            sort_keys=True, ensure_ascii=False,
        )
        hitung = hashlib.sha256(
            (sebelumnya + inti).encode()
        ).hexdigest()[:12]
        if hitung != entri["hash"]:
            return False, entri["seq"]
        sebelumnya = entri["hash"]
    return True, None


print()
utuh, rusak = verifikasi(jejak)
print(f"verifikasi rantai : {'UTUH' if utuh else 'GALAT di entri ' + str(rusak)} "
      f"({len(jejak)} entri)")

# ---------- Uji kepekaan: ubah satu entri lama secara diam-diam ----------
jejak[0]["detail"]["baris"] = 599
utuh2, rusak2 = verifikasi(jejak)
print(f"uji kepekaan       : {'UTUH' if utuh2 else 'GALAT'} "
      f"- perubahan entri {rusak2} terdeteksi")

# ---------- Gerbang manusia untuk versi berikutnya ----------
ada_reviu = any(
    e["kejadian"] == "reviu-manusia"
    and e["detail"]["keputusan"] == "setuju-dirilis"
    for e in jejak
)
insiden_terbuka = any(e["kejadian"] == "insiden-drift" for e in jejak)
print()
print("== Gerbang manusia untuk rilis versi berikutnya ==")
print(f"reviu manusia v1.0 tersedia : {'ya' if ada_reviu else 'tidak'}")
print(f"insiden drift terbuka       : {'ya' if insiden_terbuka else 'tidak'}")
print("keputusan rilis v1.1        : TAHAN - insiden harus ditutup "
      "dengan retraining dan reviu ulang")

Output yang Dihasilkan:

text
== Monitoring batch produksi terhadap data referensi ==
fitur             |     z | status
penghasilan_juta  |  0.00 | stabil
masa_kerja_tahun  |  0.00 | stabil
rasio_utang       |  3.60 | eskalasi

== Jejak audit penapis-kredit-mikro (rantai hash) ==
 1 | cbf260f31f72 | data-masuk         | pipeline-data  | {"baris": 600, "hash_dataset": "77f0ce2b300e"}
 2 | 1739e61e84f4 | latih-model        | pipeline-latih | {"akurasi_uji": 0.757, "algoritma": "LogisticRegression"}
 3 | 4d2bed594e74 | kartu-model-dibuat | sistem         | {"versi": "1.0"}
 4 | 60d228e70e4f | reviu-manusia      | Dewi           | {"catatan": "metrik dapat diterima", "keputusan": "setuju-dirilis"}
 5 | 28bde5a61d3d | insiden-drift      | monitoring     | {"ambang": 3.0, "fitur": "rasio_utang", "z": 3.6}
 6 | ab07c46a7d65 | respons-insiden    | Siti           | {"aksi": "kembali ke keputusan manual", "tindak_lanjut": "retraining lalu reviu ulang kartu model"}

verifikasi rantai : UTUH (6 entri)
uji kepekaan       : GALAT - perubahan entri 1 terdeteksi

== Gerbang manusia untuk rilis versi berikutnya ==
reviu manusia v1.0 tersedia : ya
insiden drift terbuka       : ya
keputusan rilis v1.1        : TAHAN - insiden harus ditutup dengan retraining dan reviu ulang

Bedah baris-per-baris untuk praktikum kedua:

  1. Baris 43–46 (sidik jari dataset): hashlib.sha256 atas data.to_csv(index=False) menghasilkan identitas byte-level dataset — dua dataset identik menghasilkan hash sama, satu byte berbeda mengubahnya; entri pertama jejak kini terikat pada isi data yang persis.
  2. Baris 52–68 (catat): fungsi ini adalah seluruh mekanismenya — entri baru mengambil hash entri terakhir (atau "0"*12 untuk entri pertama), membuat representasi JSON terurut kunci, lalu menghitung SHA-256 atas gabungan keduanya. Urutan entri ikut terjaga karena seq dan hash sebelumnya sama-sama masuk muatan.
  3. Baris 71–76 (empat entri pertama): alur hidup normal tercatat lengkap — data, pelatihan beserta akurasi, kartu model, dan reviu manusia oleh Dewi dengan keputusan eksplisit; setiap entri menyebut aktor (pipeline atau manusia) sehingga pertanggungjawaban tidak menggantung.
  4. Baris 82–94 (monitoring): tabel z per fitur membandingkan batch baru dengan data referensi; hanya rasio_utang yang melewati ambang 3,0 — pola pemicu tunggal yang menunjukkan pergeseran terlokalisasi, bukan galat sistemik.
  5. Baris 97–103 (respons tercatat): insiden dan responsnya masuk jejak sebagai entri — aksi “kembali ke keputusan manual” dan tindak lanjut retraining; keputusan rollback ke jalur manual ini adalah padanan champion-challenger dari Chapter Pembaruan Model dan Retraining dalam wujud yang bisa diaudit.
  6. Baris 114–139 (verifikasi dan uji kepekaan): verifikasi menghitung ulang seluruh rantai dari awal dan melaporkan UTUH; lalu satu entri lama diubah diam-diam (baris 600 menjadi 599) dan verifikasi kedua melaporkan GALAT — bukti bahwa jejak ini bukan sekadar catatan, melainkan catatan yang membuktikan dirinya sendiri.
  7. Baris 141–153 (gerbang manusia): keputusan rilis v1.1 ditahan karena masih ada insiden terbuka — aturan gerbang dievaluasi dari isi jejak, bukan dari ingatan orang.

Cara membaca hasilnya: rantai enam entri terverifikasi UTUH, insiden terdeteksi dari data (z = 3,60), responsnya tercatat dengan aktor dan tindak lanjut, dan rilis berikutnya tertahan secara otomatis. Perhatikan pula hash 12 karakter per entri — ringkas, tetapi cukup untuk membuat pemalsuan tanpa jejak menjadi kerja berat.

Monitoring dan Respons Insiden #

Bagian monitoring pada praktikum kedua adalah versi mini dari materi Section Deployment dan Operasional: Chapter Monitoring dan Deteksi Drift memakai PSI dan Kolmogorov–Smirnov untuk mengukur pergeseran distribusi, sementara praktikum ini memakai z-score rerata — cukup untuk satu fitur, tetapi PSI lebih tepat untuk profil distribusi penuh. Prinsip yang tidak berubah: monitoring tanpa runbook hanya menghasilkan laporan yang tidak pernah ditindaklanjuti. Runbook minimum berisi enam langkah berurutan.

  1. Deteksi — pemicu terukur (ambang PSI atau z) menyala pada fitur atau metrik tertentu.
  2. Catat — insiden masuk jejak audit lengkap dengan pemicu, waktu relatif, dan penanggung jawab.
  3. Tahan — promosi versi baru dihentikan; keputusan otomatis berisiko tinggi dibekukan sementara.
  4. Pulihkan — kembali ke versi terakhir yang sehat atau ke jalur keputusan manual.
  5. Atasi akar masalah — retraining dengan data baru, perbaikan fitur, atau koreksi hulu data.
  6. Tutup dengan reviu — insiden ditutup hanya setelah kartu model versi baru direviu dan gerbang manusia dilewati.

Urutan ini sengaja menempatkan pemulihan sebelum perbaikan akar masalah: pengguna sistem tidak perlu menunggu retraining selesai hanya untuk mendapatkan keputusan yang wajar kembali.

Reviu Manusia: Gerbang Terakhir #

Keterlibatan manusia pada sistem machine learning memiliki tiga tingkat yang berbeda biaya dan jangkauannya.

TingkatBentukCocok untuk
Human-in-the-loopSetiap keputusan melewati persetujuan manusiaKeputusan berdampak besar dan jarang (persetujuan kredit besar)
Human-on-the-loopManusia mengaudit sampel keputusan dan bisa menghentikan sistemKeputusan bervolume tinggi (penapisan transaksi)
Human-in-commandManusia menetapkan kebijakan, ambang, dan berwenang menghentikan sistemTata kelola keseluruhan sistem

Tingkat yang lebih dalam bukan selalu lebih baik — human-in-the-loop pada sistem bervolume tinggi menciptakan antrean dan kelelahan reviu yang justru menurunkan mutu keputusan. Praktikum kedua menunjukkan kompromi yang lazim: keputusan harian berjalan otomatis, tetapi rilis versi baru dan penutupan insiden wajib melewati manusia — gerbang yang frekuensinya rendah namun konsekuensinya besar.

Checklist Go/No-Go sebelum Rilis #

Seluruh section ini akhirnya bisa diringkas menjadi satu checklist yang diperiksa sebelum model dipromosikan ke produksi.

  • Data: minimisasi diterapkan; dasar pemrosesan terdokumentasi (GDPR/UU PDP); tidak ada quasi-identifier yang tidak perlu.
  • Evaluasi: metrik sesuai konteks bisnis; data uji terpisah jujur; base rate per kelompok diketahui.
  • Fairness: metrik per kelompok dihitung bila atribut tersedia; definisi fairness yang dipilih ditulis beserta alasannya.
  • Keandalan: robust accuracy dilaporkan untuk sistem yang inputnya bisa diganggu (Chapter Adversarial Examples).
  • Dokumentasi: kartu model dibangun dari artefak; batasan dan pertimbangan privasi tertulis.
  • Operasional: monitoring aktif dengan pemicu eskalasi; runbook respons insiden teruji; jalur rollback jelas.
  • Tata kelola: jejak audit berjalan dan diverifikasi; reviu manusia tercatat; gerbang rilis aktif.

Checklist ini terasa panjang saat pertama kali dijalankan, tetapi setiap barisnya sudah pernah kita praktikkan satu per satu di sepanjang Course. Section berikutnya justru tempat terbaik untuk membiasakannya: proyek pertama kita, Proyek: Prediksi Harga Rumah, menyediakan siklus lengkap — eksplorasi data, preprocessing, pelatihan, evaluasi, hingga laporan — di mana dokumentasi dan jejak audit bisa tumbuh bersama kode sejak baris pertama, bukan ditempelkan setelah selesai.

Referensi Resmi #