Model hanya sebaik dataset yang mengukurnya. Sebelum satu pun algoritma dipilih, data engineer sudah menentukan batas atas kualitas model lewat cara menyusun dataset training: baris apa yang boleh masuk, fitur apa yang sah, dan bagaimana data dibagi menjadi bagian latih dan bagian uji. Chapter ini membahas dua artefak tersebut — dataset training dan data inference — lalu menunjukkan lewat eksperimen bahwa satu keputusan pembagian yang salah mampu membuat metrik evaluasi berbohong dua kali lipat.

Dua Wajah Data Model #

Dataset training dan data inference hidup di dunia yang berbeda. Dataset training adalah tabel masa lalu: setiap baris memuat fitur plus label yang sudah diverifikasi, dan seluruh tabel tersedia sekaligus untuk proses latihan. Data inference adalah kebalikannya: satu baris fitur tanpa label, tiba pada saat request scoring dibuat, dan harus dijawab dalam waktu milidetik. Model menjadi jembatan antara keduanya, sehingga kualitas model sepenuhnya bergantung pada seberapa mirip dataset training meniru situasi inference.

ArtefakIsiKapan dibuat
Dataset trainingfitur plus label, jutaan baris historissebelum latihan, dibekukan dari masa lalu
Data inferencesatu baris fitur, tanpa labelsaat request scoring tiba, dihitung saat itu

Kesamaan bentuk saja tidak cukup; kesamaan waktu yang lebih penting. Baris training dibentuk dari kejadian berbulan-bulan yang lalu, sedangkan baris inference menggambarkan hari ini.

Semakin jauh jarak kedua dunia tersebut — pelanggan baru yang belum pernah dilihat model, perilaku belanja yang berubah, nilai transaksi yang bergeser — semakin besar selisih performa antara saat latihan dan saat produksi.

Konsekuensinya konkret untuk perancangan dataset. Label harus didefinisikan dari kejadian yang sudah selesai (misalnya transaksi terkonfirmasi fraud oleh tim riset, bukan sedang diselidiki), dan setiap kolom pada dataset training harus punya padanan yang bisa diisi pada saat serving. Kolom tanpa padanan serving adalah fitur bocor menunggu waktu — pelajaran utama chapter Kebutuhan Data Machine Learning.

Pembagian yang Meniru Cara Model Dipakai #

Cara paling umum membagi dataset adalah acak: campur seluruh baris, ambil 70% untuk latih, sisakan 30% untuk uji. Untuk data lintas waktu, cara ini diam-diam melanggar kontrak waktu dari chapter Kebutuhan Data Machine Learning, karena baris uji bisa berasal dari masa sebelum baris latih. Eksperimen berikut membandingkannya dengan pembagian temporal yang meniru produksi: model dilatih dari hari 1-20, lalu diuji pada hari 21-30. Keduanya dievaluasi dengan model dan metrik yang sama supaya satu-satunya perbedaan yang tersisa hanyalah strategi pemotongannya.

Skrip lengkap membuat 188 transaksi dari 12 pelanggan selama 30 hari Oktober 2026 dengan generator ber-seed pada modul random Python; tiga pelanggan (c10, c11, c12) baru aktif mulai hari ke-21, meniru pelanggan baru di produksi. Blok berikut memotong bagian generator tersebut dan menampilkan inti eksperimennya:

python
def mae_evaluasi(train, test):
    """Model lookup: prediksi nilai = rata-rata pelanggan di train (fallback global)."""
    rata_pelanggan = train.groupby("customer_id")["nilai"].mean()
    fallback = train["nilai"].mean()
    prediksi = test["customer_id"].map(rata_pelanggan).fillna(fallback)
    belum_dilihat = ~test["customer_id"].isin(rata_pelanggan.index)
    mae = (prediksi - test["nilai"]).abs().mean()
    return mae, belum_dilihat.mean(), test["customer_id"][belum_dilihat].unique().tolist()


# --- Pembagian acak: campur seluruh 30 hari, ambil 70% --------------------------
acak = df.sample(frac=0.7, random_state=2026)
sisa = df.drop(acak.index)
mae_acak, tak_terlihat_acak, pelanggan_baru_acak = mae_evaluasi(acak, sisa)

# --- Pembagian temporal: train hari 1-20, test hari 21-30 -----------------------
train_t = df[df["tanggal"] <= date(2026, 10, 20)]
test_t = df[df["tanggal"] > date(2026, 10, 20)]
mae_temporal, tak_terlihat, pelanggan_baru = mae_evaluasi(train_t, test_t)

Mari kita telaah baris-baris kode di atas:

  1. Baris 1-10 (def mae_evaluasi): sebagai pengganti model ML, kita memakai model lookup — prediksi nilai transaksi berikutnya sama dengan rata-rata pelanggan tersebut di data latih; pelanggan yang tidak dikenal memakai rata-rata global sebagai fallback.
  2. Baris 13-15 (acak = df.sample ...): pembagian acak mengocok seluruh 30 hari, sehingga setiap pelanggan hampir pasti muncul di kedua sisi.
  3. Baris 18-20 (train_t = df ...): pembagian temporal memotong waktu secara tegas — tidak ada satu baris hari ke-21 ke atas yang boleh disentuh model sebelum pengujian.

Output yang Dihasilkan:

text
total 188 transaksi, 12 pelanggan, hari 1-30

== Pembagian acak (70/30, dicampur lintas waktu) ==
  MAE                 : Rp26,167
  baris test dgn pelanggan tak ada di train: 0%
  pelanggan test tak ada di train: []

== Pembagian temporal (train hari 1-20, test hari 21-30) ==
  MAE                 : Rp53,348
  baris test dgn pelanggan tak ada di train: 24%
  pelanggan test tak ada di train: ['c10', 'c11', 'c12']

MAE temporal 2.04x lebih besar dari MAE acak -- pembagian acak terlalu optimis sebagai perkiraan produksi.

Angka-angka tersebut dihasilkan oleh bagian pelaporan skrip yang mencetak metrik kedua pembagian berdampingan:

python
print(f"total {len(df)} transaksi, {df.customer_id.nunique()} pelanggan, hari 1-30")
print()
print("== Pembagian acak (70/30, dicampur lintas waktu) ==")
print(f"  MAE                 : Rp{mae_acak:,.0f}")
print(f"  baris test dgn pelanggan tak ada di train: {tak_terlihat_acak:.0%}")
print(f"  pelanggan test tak ada di train: {pelanggan_baru_acak}")
print()
print("== Pembagian temporal (train hari 1-20, test hari 21-30) ==")
print(f"  MAE                 : Rp{mae_temporal:,.0f}")
print(f"  baris test dgn pelanggan tak ada di train: {tak_terlihat:.0%}")
print(f"  pelanggan test tak ada di train: {sorted(pelanggan_baru)}")
print()
print(f"MAE temporal {mae_temporal / mae_acak:.2f}x lebih besar dari MAE acak -- "
      f"pembagian acak terlalu optimis sebagai perkiraan produksi.")

Format pelaporan ini meletakkan kedua pembagian berdampingan, sehingga selisih 2.04x terlihat sebagai fakta yang menuntut penjelasan, bukan tenggelam di dua log berbeda.

Angka yang Jujur versus Angka yang Menyenangkan #

AspekPembagian acakPembagian temporal
MAE hasil ukurRp26.167Rp53.348
Pelanggan tak dikenal di test0%24%
Informasi masa depanikut tercampur ke trainsepenuhnya di luar train
Angka yang dilaporkanperkiraan terlalu optimisperkiraan realistis produksi
Pelanggan baru (cold start)nyaris tak terujiteruji langsung
Cocok untukdata tanpa dimensi waktudata lintas waktu

Kedua pembagian memakai model, data, dan metrik yang sama; satu-satunya beda adalah arah pisau pemotong. Pembagian acak menghasilkan MAE Rp26.167 karena setiap pelanggan di sisi uji sudah dikenal baik-baik oleh sisi latih — informasi yang tidak akan pernah tersedia di produksi. Pembagian temporal menghukum model dengan situasi nyata: 24% baris uji berasal dari pelanggan yang belum pernah dilihat (cold start), dan MAE melonjak ke Rp53.348. Angka yang lebih besar inilah yang jujur, karena hari demi hari model memang harus melayani pelanggan baru.

Praktik industri menambahkan satu lapis lagi di atas kedua pembagian ini: validation set untuk memilih model dan test set yang hanya disentuh sekali di akhir. Aturan mainnya sama — test set adalah simulasi produksi, jadi potong menurut waktu, dan jangan pernah melaporkan metrik dari data yang pernah dipakai mengambil keputusan.

Prinsip jujur waktu ini baru menyentuh cara memotong dataset. Pertanyaan berikutnya: dari mana baris-baris berfitur tersebut berasal sebelum bisa dipotong? Jawabannya adalah pipeline yang mengubah event mentah menjadi fitur terstruktur per entitas — materi chapter Feature Pipeline yang menyusul setelah ini.