Validasi adalah gerbang pertama pipeline: lebih baik menolak baris yang tidak dapat dipercaya daripada membiarkannya mencemari tabel yang dipakai pengambilan keputusan. Pada bagian ini kita membaca berkas CSV, membandingkan header dengan kontrak kolom, lalu memeriksa setiap baris terhadap aturan yang diturunkan dari audit Bagian 1. Modul src/validate.py ditulis sebagai kumpulan fungsi murni tanpa efek samping, sehingga perilakunya dapat diuji tanpa menyentuh berkas lain.

Kontrak Kolom #

Dua fungsi pertama pada modul menjaga bentuk berkas. Fungsi baca_csv memakai csv.DictReader agar setiap baris berupa dictionary nama kolom -> nilai, bukan daftar posisi:

python
def baca_csv(path: str) -> tuple[list[str], list[dict]]:
    """Baca satu berkas CSV; kembalikan (header, daftar baris)."""
    with open(path, newline="", encoding="utf-8") as f:
        pembaca = csv.DictReader(f)
        header = [k for k in (pembaca.fieldnames or []) if k]
        baris = [dict(r) for r in pembaca]
    return header, baris

Fungsi cek_kolom membandingkan header dengan KOLOM_WAJIB. Kedua arah diperiksa: kolom wajib yang hilang, dan kolom asing yang tidak dikenal:

python
def cek_kolom(header: list[str]) -> list[str]:
    """Bandingkan header berkas dengan KOLOM_WAJIB.

    Kembalikan daftar keterangan; daftar kosong berarti header sesuai.
    """
    keterangan: list[str] = []
    for kolom in KOLOM_WAJIB:
        if kolom not in header:
            keterangan.append(f"kolom wajib hilang: {kolom}")
    for kolom in header:
        if kolom and kolom not in KOLOM_WAJIB:
            keterangan.append(f"kolom tak dikenal: {kolom}")
    return keterangan

Bila header tidak sesuai, pipeline berhenti sebelum memproses satu baris pun. Header yang salah berarti berkas bukan yang kita sangka, dan melanjutkannya hanya menyembunyikan galat.

Aturan Per Kolom #

Setiap kolom punya aturan yang dapat ditunjuk asal-usulnya dari audit. Hasilnya dirangkum dalam tabel berikut:

KolomAturanKode masalah bila dilanggar
order_idcocok pola ORD-YYYY-NNNNorder_id_tidak_valid
tanggaltanggal nyata berformat YYYY-MM-DDtanggal_tidak_valid
emailcocok pola [email protected]email_tidak_valid
kategorisalah satu dari lima kategorikategori_tidak_dikenal
jumlahangka bulat minimal 1jumlah_tidak_positif
hargaangka >= 0, boleh berpemisah ribuanharga_tidak_valid
statussalah satu dari empat statusstatus_tidak_dikenal
pelanggan, produktidak kosongpelanggan_kosong, produk_kosong

Perhatikan dua keputusan yang tampak kecil tetapi berdampak besar. Pertama, pola order_id dicocokkan dengan re.IGNORECASE, sehingga ord-2024-0006 lolos validasi dan cukup dibesarkan pada tahap pembersihan. Kedua, harga menerima 210.000 karena format ribuan adalah kebiasaan penulisan yang sah, bukan kebohongan data; tugas menormalkannya diberikan ke Bagian 4.

Validasi Satu Baris #

Satu baris diperiksa oleh validasi_baris, yang mengembalikan daftar objek Masalah:

python
@dataclass
class Masalah:
    """Satu temuan validasi pada satu baris."""

    baris: int  # nomor baris di berkas, 1-based termasuk baris header
    kolom: str
    jenis: str  # kode masalah yang stabil untuk pelaporan
    pesan: str

Cuplikan berikut menunjukkan tiga aturan pertama di dalam validasi_baris; aturan lainnya mengikuti pola yang sama:

python
order_id = teks("order_id")
if not order_id:
    galat("order_id", "order_id_kosong", "order_id tidak boleh kosong")
elif not POLA_ORDER_ID.match(order_id):
    galat("order_id", "order_id_tidak_valid", "pola order_id harus ORD-YYYY-NNNN")

tanggal = teks("tanggal")
if not tanggal:
    galat("tanggal", "tanggal_kosong", "tanggal tidak boleh kosong")
else:
    try:
        datetime.strptime(tanggal, "%Y-%m-%d")
    except ValueError:
        galat(
            "tanggal",
            "tanggal_tidak_valid",
            "tanggal harus tanggal nyata berformat YYYY-MM-DD",
        )

email = teks("email")
if not email:
    galat("email", "email_kosong", "email tidak boleh kosong")
elif not POLA_EMAIL.match(email):
    galat("email", "email_tidak_valid", "email harus berformat [email protected]")

Mari kita telaah maksudnya:

  1. teks(kolom): fungsi kecil yang membaca kolom lalu memangkas spasi tepi. Nilai " 2 " tetap valid karena spasi bukan kebohongan data.
  2. datetime.strptime(...): tanggal 2024-06-31 lolos pola teks YYYY-MM-DD, tetapi strptime menolaknya karena 31 Juni tidak ada di kalender. Pemeriksaan kalender inilah yang membedakan validasi tanggal sungguhan dari pencocokan pola.
  3. galat(kolom, jenis, pesan): setiap pelanggaran membawa nomor baris, kolom, kode stabil, dan kalimat yang dapat dibaca operator. kode jenis menjadi kunci pelaporan dan pengujian.

Menjalankan Validasi pada Data Asli #

Ringkasan berikut dijalankan nyata terhadap berkas Juni. Seluruh temuan lalu dihitung per jenis dengan ringkas_masalah:

python
header, baris = validate.baca_csv("data/pesanan_2024-06.csv")
temuan = []
ditolak = 0
for nomor, b in enumerate(baris, start=2):
    t = validate.validasi_baris(b, nomor)
    if t:
        ditolak += 1
    temuan += t

Output yang Dihasilkan:

text
berkas : data/pesanan_2024-06.csv
baris  : 107 (16 ditolak, 91 lolos)
temuan : 16
ringkasan per jenis:
  email_kosong           1
  email_tidak_valid      1
  harga_kosong           1
  harga_negatif          1
  harga_tidak_valid      1
  jumlah_tidak_positif   2
  kategori_tidak_dikenal 1
  order_id_kosong        1
  order_id_tidak_valid   1
  pelanggan_kosong       1
  produk_kosong          1
  status_tidak_dikenal   1
  tanggal_kosong         1
  tanggal_tidak_valid    2
contoh temuan:
  baris   9 | email     | email_tidak_valid    | email harus berformat [email protected]
  baris  14 | jumlah    | jumlah_tidak_positif | jumlah tidak boleh negatif
  baris  20 | harga     | harga_kosong         | harga tidak boleh kosong
  baris  25 | tanggal   | tanggal_tidak_valid  | tanggal harus tanggal nyata berformat YYYY-MM-DD
  baris  32 | order_id  | order_id_tidak_valid | pola order_id harus ORD-YYYY-NNNN
  baris  38 | kategori  | kategori_tidak_dikenal | kategori harus salah satu dari: buku, elektronik, fashion, makanan, olahraga

Angka pada ringkasan persis seperti rancangan audit: 16 baris ditolak, masing-masing dengan satu temuan. Setiap jenis muncul dari contoh nyata yang kita tanam di berkas, bukan dari aturan yang ditebak.

Rujukan teknis untuk bagian ini: csv.DictReader dan modul re .

Validasi menolak baris yang tidak dapat diselamatkan. Tetapi 91 baris yang lolos belum berarti siap dipakai: kapitalisasi, spasi, dan pemisah ribuan masih melekat. Tugas itulah yang kita kerjakan berikutnya. Lanjutkan ke Bagian 4: Membersihkan Data.