Setiap eksperimen machine learning adalah hasil kali tiga hal: kode model, hyperparameter, dan dataset. Git menangani yang pertama dengan baik, namun dataset justru berubah setiap hari — pesanan baru masuk, nilai lama dikoreksi, kolom ditambahkan — sehingga pertanyaan paling mendasar dalam audit model menjadi sulit dijawab: model yang dirilis bulan lalu dilatih dari data yang persis seperti apa? Tanpa versioning dataset, jawabannya hanya tebakan. Chapter ini membangun versioning dataset sederhana namun lengkap: snapshot berversi, manifest ber-hash, diff antar versi, dan pin di konfigurasi training.

Kenapa Tidak Cukup Menyimpan File Saja #

Menyimpan salinan orders_2026_11_01.parquet di folder arsip menyelesaikan setengah masalah: file lama masih ada, tetapi tidak ada yang menautkannya ke model yang memakainya, dan tidak ada cara memverifikasi file tersebut tidak berubah sejak disimpan. Versioning dataset memerlukan tiga komponen yang bekerja bersama:

KomponenPeran
Snapshot per versidata beku yang tidak pernah ditimpa
Manifest ber-hashidentitas kriptografis isi snapshot
Pin di config trainingtautan resmi model ke satu versi dataset

Pola ini sebangun dengan Pipeline Versioning yang membahas versi kode pipeline; bedanya, di sini yang dibekukan adalah artefak data, bukan kode yang memprosesnya. Perlu pula dibedakan dari Snapshots pada section analytics: snapshot dbt melacak perubahan baris dari waktu ke waktu untuk kebutuhan analitik, sedangkan versioning dataset membekukan salinan lengkap agar sebuah proses training bisa direproduksi persis.

Snapshot dan Manifest Ber-Hash #

Skrip pada berkas scratch membuat dua versi dataset orders: versi v1 berisi 6 pesanan, lalu v2 berisi 2 nilai yang dikoreksi plus 3 pesanan baru. Fungsi berikut menjadi jantung keduanya:

python
def tulis_manifest(versi, df):
    """Simpan snapshot parquet + manifest sha256 untuk satu versi dataset."""
    folder = ROOT / versi
    folder.mkdir(parents=True, exist_ok=True)
    berkas = folder / "data.parquet"
    df.to_parquet(berkas, index=False)
    sha = hashlib.sha256(berkas.read_bytes()).hexdigest()
    manifest = {
        "versi": versi,
        "berkas": "data.parquet",
        "sha256": sha,
        "jumlah_baris": len(df),
        "kolom": list(df.columns),
    }
    (folder / "manifest.json").write_text(json.dumps(manifest, indent=2))
    return manifest

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-6 (folder = ... s.d. df.to_parquet): setiap versi mendapat folder sendiri (v1/, v2/), sehingga menulis versi baru tidak pernah menyentuh versi lama.
  2. Baris 7 (sha = hashlib.sha256 ...): seluruh isi data.parquet diringkas menjadi satu sidik jari 64 karakter menggunakan modul hashlib — satu byte berubah, hash berubah total.
  3. Baris 8-14 (manifest = ...): manifest mencatat hash, jumlah baris, dan daftar kolom; angka-angka ini memudahkan pemeriksaan cepat sebelum file dibuka sama sekali.

Output yang Dihasilkan:

text
== Manifest ==
  v1: 6 baris, sha256 6935b960bf2ffa38...
  v2: 9 baris, sha256 c8d729bc11e18b64...

Diff Antar Versi #

Ketika dataset berubah, tim perlu tahu apa yang berubah tanpa membedah kedua file secara manual. Diff dilakukan dengan merge outer berdasarkan kunci order_id:

python
gabung = v1.merge(v2, on="order_id", how="outer", suffixes=("_v1", "_v2"))
ditambah = gabung["customer_id_v1"].isna().sum()
dihapus = gabung["customer_id_v2"].isna().sum()
berubah = ((gabung["total_v1"] != gabung["total_v2"]) & gabung["total_v1"].notna()).sum()
tetap = len(gabung) - ditambah - dihapus - berubah
print()
print("== Diff v1 -> v2 ==")
print(f"  baris tetap: {tetap}, berubah: {berubah}, ditambah: {ditambah}, dihapus: {dihapus}")

Baris yang hanya ada di v2 membuat kolom _v1 kosong (isna), baris yang hilang sebaliknya, dan baris yang ada di keduanya tetapi bernilai total berbeda dihitung sebagai berubah. Diff berbasis kunci seperti ini lebih aman daripada membandingkan urutan baris, karena urutan hasil query bisa berubah tanpa makna. Hasilnya ringkas dan bisa dibaca cepat oleh siapa pun yang menimbang apakah model perlu dilatih ulang:

Output yang Dihasilkan:

text
== Diff v1 -> v2 ==
  baris tetap: 4, berubah: 2, ditambah: 3, dihapus: 0

Pin Versi di Training Config #

Komponen terakhir menautkan model ke dataset: konfigurasi training menyimpan nama versi plus hash manifest-nya, membentuk rantai verifikasi berlapis — config memverifikasi manifest, manifest memverifikasi data:

python
config_path = ROOT / "training_config.yaml"
config_path.write_text(
    yaml.safe_dump(
        {
            "model": "deteksi_fraud",
            "dataset": {
                "nama": "orders",
                "versi": "v2",
                "manifest_sha256": hashlib.sha256(
                    (ROOT / "v2" / "manifest.json").read_bytes()
                ).hexdigest(),
            },
        }
    )
)
config = yaml.safe_load(config_path.read_text())
manifest_v2 = json.loads((ROOT / config["dataset"]["versi"] / "manifest.json").read_text())
hash_manifest = hashlib.sha256((ROOT / "v2" / "manifest.json").read_bytes()).hexdigest()
hash_data = hashlib.sha256((ROOT / "v2" / "data.parquet").read_bytes()).hexdigest()

Dua baris terakhir menghitung ulang kedua hash untuk validasi — pola yang sebaiknya dijalankan sebagai gerbang sebelum training dimulai:

Output yang Dihasilkan:

text
== Validasi pin di training config ==
  config -> versi v2, manifest 7a15aae2910c1cfd...
  manifest cocok dengan pin : True
  data.parquet cocok dengan manifest: True

Dengan pola ini, menjawab “model ini dilatih dari data apa” cukup membaca satu berkas YAML — dan bila seseorang mengubah isi data.parquet satu byte saja, validasi pin langsung gagal dengan selisih hash. Alat seperti DVC atau lakeFS memperbesar pola yang sama ke skala besar; prinsipnya tidak berubah. Versi menjamin kita mendapat data yang sama; validasi berikutnya menjamin data tersebut benar — materi chapter Data Validation untuk ML.