Ada titik di mana mengumpulkan data asli terlalu mahal, terlalu lambat, atau terlalu sensitif: data medis milik pasien, kejadian penipuan yang baru terjadi belasan kali, kondisi batas yang tidak pernah tercatat. Pada titik itu tim melirik data sintetis — data baru yang dibangkitkan dari aturan atau dari model yang mempelajari distribusi data asli. Chapter ini membangun generator tabular berbasis aturan, memvalidasi kemiripannya secara kuantitatif, menguji kegunaannya lewat train-on-synthetic test-on-real, dan menutup dengan risiko yang tidak boleh diabaikan.

Kapan Data Sintetis Dibutuhkan #

Empat pendorong paling umum:

  • Privasi — data asli mengandung identitas pemiliknya; data sintetis tidak merujuk individu sungguhan.
  • Kejadian langka — kelas dengan 20 sampel tidak cukup untuk training; sintetis mengisi wilayah datanya.
  • Uji kasus batas — perilaku model perlu diuji pada input ekstrem yang jarang muncul di alam.
  • Pengembangan awal — pipeline dan arsitektur dapat dikembangkan sambil menunggu data asli terkumpul.

Tiga Metode Pembangkitan #

MetodeCara kerjaKapan cocokRisiko utama
Berbasis aturananalis menulis constraint dan distribusi sebagai kodestruktur data dipahami baikbias analis ikut tertanam
Sampling dari modelGAN/diffusion/vae mempelajari distribusi lalu membangkitkandata kompleks (gambar, sinyal)sampel berkualitas rendah saat mode collapse
LLM-generatedmodel bahasa menuliskan contoh barudata teks prototipehalusinasi dan pengulangan pola teks

Metode sampling dari model dibahas mendalam di section Generative Model ; chapter ini berfokus pada metode berbasis aturan karena paling transparan — setiap keputusan distributor terbaca di kode.

Demo: Generator, Validasi, Uji Kegunaan #

Berkas data_sintetis.py mensimulasikan alur kerja lengkap: 2.000 baris “data asli” nasabah fiktif, generator aturan yang menirunya, perbandingan statistik, pemeriksaan constraint, dan model yang dilatih murni pada data sintetis lalu diuji pada data asli:

python
"""Synthetic data: generator berbasis aturan + validasi distribusi.

Skenario: "data asli" 2.000 baris berisi nasabah fiktif (usia,
pendapatan, kota). Seorang analis membangun generator sintetis dari
aturan sederhana yang meniru statistik data asli, lalu kita validasi
kemiripannya dan uji kegunaannya: model dilatih pada data sintetis,
dievaluasi pada data asli.
"""

import numpy as np

rng = np.random.default_rng(42)      # sumber keacakan data asli
rng_sint = np.random.default_rng(123)  # sumber keacakan generator

referensi = np.array(["Jakarta", "Bandung", "Surabaya"])


def bangun_asli(n):
    """Populasi asli: usia campuran dua kelompok, kota bergantung pendapatan."""
    usia = np.where(
        rng.random(n) < 0.6,
        rng.normal(33, 8, n),
        rng.normal(47, 7, n),
    )
    usia = np.clip(usia, 18, 65)
    pendapatan = 2.0 + 0.2 * (usia - 18) + rng.lognormal(0, 0.45, n)
    p_jakarta = np.clip(0.3 + 0.06 * pendapatan, 0, 0.85)
    p_kota = np.column_stack([p_jakarta, (1 - p_jakarta) * 0.6,
                              (1 - p_jakarta) * 0.4])
    kota = np.array(
        [referensi[np.argmax(rng.multinomial(1, p))] for p in p_kota]
    )
    return usia, pendapatan, kota


def bangun_sintetis(n):
    """Generator analis: aturan sederhana meniru statistik data asli."""
    usia = np.clip(rng_sint.normal(39, 11, n), 18, 65)
    pendapatan = 2.5 + 0.19 * (usia - 18) + rng_sint.normal(0, 1.4, n)
    pendapatan = np.clip(pendapatan, 0.5, None)
    kota = rng_sint.choice(referensi, n, p=[0.5, 0.3, 0.2])
    return usia, pendapatan, kota


# ---------- 1. Data asli dan data sintetis ----------
usia_asli, pendapatan_asli, kota_asli = bangun_asli(2000)
usia_syn, pendapatan_syn, kota_syn = bangun_sintetis(2000)

# ---------- 2. Validasi distribusi ----------
print("===== VALIDASI DISTRIBUSI (asli vs sintetis) =====")
print(f"{'statistik':28s}{'asli':>10s}{'sintetis':>10s}")
print(f"{'usia: mean':28s}{usia_asli.mean():10.2f}{usia_syn.mean():10.2f}")
print(f"{'usia: std':28s}{usia_asli.std():10.2f}{usia_syn.std():10.2f}")
print(f"{'pendapatan: mean':28s}{pendapatan_asli.mean():10.2f}"
      f"{pendapatan_syn.mean():10.2f}")
print(f"{'pendapatan: std':28s}{pendapatan_asli.std():10.2f}"
      f"{pendapatan_syn.std():10.2f}")

urut = np.argsort(referensi)  # Bandung, Jakarta, Surabaya
prop_asli = np.unique(kota_asli, return_counts=True)[1] / 2000
prop_syn = np.unique(kota_syn, return_counts=True)[1] / 2000
print("kota proporsi asli    :", {str(k): round(float(v), 3)
                                   for k, v in zip(referensi[urut], prop_asli)})
print("kota proporsi sintetis:", {str(k): round(float(v), 3)
                                   for k, v in zip(referensi[urut], prop_syn)})

kor_asli = np.corrcoef(usia_asli, pendapatan_asli)[0, 1]
kor_syn = np.corrcoef(usia_syn, pendapatan_syn)[0, 1]
print(f"korelasi usia-pendapatan : {kor_asli:.3f} vs {kor_syn:.3f}")

# ---------- 3. Cek constraint tidak dilanggar ----------
print()
print("===== CEK CONSTRAINT SINTETIS =====")
print("usia dalam [18, 65]?    ",
      bool(usia_syn.min() >= 18 and usia_syn.max() <= 65))
print("pendapatan >= 0.5 semua?",
      bool(pendapatan_syn.min() >= 0.5))
print("kota hanya 3 nilai?     ",
      set(np.unique(kota_syn)) == set(referensi))

# ---------- 4. Train-on-synthetic, test-on-real ----------
def fitur(usia, pendapatan, kota):
    """Fitur model: usia, pendapatan, one-hot kota."""
    onehot = np.column_stack(
        [kota == k for k in referensi]
    ).astype(float)
    return np.column_stack([usia, pendapatan, onehot])


def label_aktif(usia, pendapatan, rng_label):
    """Aturan bisnis target: 'aktif' dipengaruhi usia dan pendapatan."""
    skor = 0.05 * usia + 0.35 * pendapatan + rng_label.normal(0, 1, len(usia))
    return (skor > np.median(skor)).astype(float)


F_syn = fitur(usia_syn, pendapatan_syn, kota_syn)
F_asli = fitur(usia_asli, pendapatan_asli, kota_asli)
y_syn = label_aktif(usia_syn, pendapatan_syn, rng_sint)
y_asli = label_aktif(usia_asli, pendapatan_asli, rng)

# standardisasi memakai statistik sintetis saja
mu, sigma = F_syn.mean(axis=0), F_syn.std(axis=0)
Z_syn = (F_syn - mu) / sigma
Z_asli = (F_asli - mu) / sigma

# regresi logistic mini dengan NumPy (sigmoid numerik stabil)
w = np.zeros(Z_syn.shape[1])
for _ in range(500):
    z = np.clip(Z_syn @ w, -30, 30)
    p = 1 / (1 + np.exp(-z))
    grad = Z_syn.T @ (p - y_syn) / len(y_syn)
    w -= 0.5 * grad

akurasi_syn = float(((Z_syn @ w > 0) == y_syn).mean())
akurasi_asli = float(((Z_asli @ w > 0) == y_asli).mean())
print()
print("===== TRAIN PADA SINTETIS, UJI PADA ASLI =====")
print(f"akurasi pada data sintetis: {akurasi_syn:.3f}")
print(f"akurasi pada data asli    : {akurasi_asli:.3f}")
print("selisih                   :",
      round(abs(akurasi_syn - akurasi_asli), 3))
print("(model tidak pernah melihat data asli; selisih kecil")
print(" menandakan generator menangkap relasi pentingnya.)")

Output yang Dihasilkan:

text
===== VALIDASI DISTRIBUSI (asli vs sintetis) =====
statistik                         asli  sintetis
usia: mean                       38.85     39.23
usia: std                        10.25     10.79
pendapatan: mean                  7.27      6.54
pendapatan: std                   2.12      2.44
kota proporsi asli    : {'Bandung': 0.157, 'Jakarta': 0.729, 'Surabaya': 0.115}
kota proporsi sintetis: {'Bandung': 0.311, 'Jakarta': 0.5, 'Surabaya': 0.189}
korelasi usia-pendapatan : 0.970 vs 0.822

===== CEK CONSTRAINT SINTETIS =====
usia dalam [18, 65]?     True
pendapatan >= 0.5 semua? True
kota hanya 3 nilai?      True

===== TRAIN PADA SINTETIS, UJI PADA ASLI =====
akurasi pada data sintetis: 0.791
akurasi pada data asli    : 0.805
selisih                   : 0.014
(model tidak pernah melihat data asli; selisih kecil
 menandakan generator menangkap relasi pentingnya.)

Contoh di atas diuji nyata dengan NumPy 2.1.3 pada mesin CPU dan selesai dalam waktu di bawah satu detik. Mari kita telaah kode di atas baris per baris:

  1. Baris 18-33 (bangun_asli): populasi asli memakai usia campuran dua kelompok (33 dan 47 tahun), pendapatan bertambah linear dengan usia plus derau lognormal, dan peluang kota bergantung pada pendapatan — relasi antar kolom inilah yang harus ditiru generator, bukan hanya rata-rata tiap kolom.
  2. Baris 36-42 (bangun_sintetis): generator analis memakai aturan lebih sederhana — usia normal tunggal, pendapatan linear plus derau gaussian, kota dengan proporsi tetap; kesederhanaan ini membuat kesalahan peniruan mudah dilacak.
  3. Baris 50-69 (validasi): rata-rata dan simpangan baku tiap kolom cocok, tetapi validasi menemukan dua ketidakcocokan: proporsi Jakarta 0.729 vs 0.500 dan korelasi usia-pendapatan 0.970 vs 0.822 — temuan seperti inilah tujuan validasi; generator perlu diperbaiki bila analisis bergantung pada kolom tersebut.
  4. Baris 71-79 (constraint): pemeriksaan batas memastikan tidak ada usia di luar [18, 65], pendapatan negatif, atau kota di luar tiga nilai sah — data sintetis tetap harus lulus aturan bisnis.
  5. Baris 82-96 (fitur dan label): aturan label_aktif menghasilkan target dari usia dan pendapatan dengan derau; aturan target sama diterapkan pada kedua dataset sehingga hasil uji adil.
  6. Baris 99-101 (standardisasi): statistik standardisasi diambil dari data sintetis saja — data asli diperlakukan murni sebagai data uji.
  7. Baris 104-110 (training): regresi logistic NumPy dengan np.clip(z, -30, 30) menjaga komputasi sigmoid stabil secara numerik; 500 pembaruan full-batch deterministik.
  8. Baris 112-121 (uji kegunaan): akurasi 0.791 pada sintetis dan 0.805 pada asli — selisih 0.014 membuktikan relasi yang dipelajari dari sintetis berlaku pada data asli.

Risiko: Model Collapse #

Sirkulasi data sintetis punya batas keras. Bila model generasi berikutnya dilatih pada keluaran model sebelumnya berulang kali — tanpa injeksi data asli segar — distribusi hasil melalui penyempitan bertahap: variasi hilang, ekor distribusi terpotong, dan akhirnya hanya tersisa modus dominan. Gejala serupa terdokumentasi pada pelatihan bahasa berskala besar di arXiv:2305.17493 . Aturan praktisnya: data sintetis melengkapi data asli, tidak menggantikannya sepenuhnya, dan setiap campuran harus dicatat proporsinya.

Motivasi terbesar data sintetis justru berada satu chapter lagi di depan: melindungi identitas pemilik data. Bagaimana mengukur perlindungan itu secara kuantitatif — dari k-anonymity hingga differential privacy — adalah bahasan penutup section ini.