Tahap extract menyerahkan data apa adanya, sehingga sekarang meja kita penuh: ada nama pelanggan yang berlumur spasi, harga 1.250.000 yang bukan angka bagi Python, status SELESAI yang bercampur huruf besar, dua baris tanpa kolom wajib, dan satu pesanan yang dikoreksi lewat baris duplikat. Tugas transform adalah mengubah tumpukan ini menjadi baris fact yang siap dimuat, tanpa kehilangan jejak baris yang ditolak. Kita memakai pandas untuk pekerjaan baris per baris karena aturannya banyak dan bercabang; agregasi per kota diserahkan ke SQL pada tahap load.
Aturan Pembersihan #
Lima aturan berikut dijalankan untuk setiap baris, urutannya menentukan hasil akhirnya:
| Aturan | Contoh pelanggar | Tindakan |
|---|---|---|
| Kolom teks wajib terisi | customer_name atau city kosong | Masuk karantina |
quantity bilangan bulat positif | -1, x, kosong | Masuk karantina |
unit_price angka positif | abc, 0 | Masuk karantina |
order_time terbaca sebagai timestamp | bukan-waktu | Masuk karantina |
order_id duplikat dalam partisi | koreksi pesanan yang sama | Kemunculan terakhir menang |
Setiap penolakan dicatat sebagai satu baris karantina berisi nomor baris CSV, order_id, alasan, dan nilai mentahnya. Nomor baris dihitung termasuk baris header, sehingga baris 8 pada berkas merujuk baris data ketujuh.
parse_harga untuk Format Indonesia #
Harga pada berkas sumber memakai dua gaya: 899000 dan 1.250.000. Fungsi kecil berikut menormalkan keduanya:
def parse_harga(teks: str) -> float:
"""Ubah teks harga Indonesia menjadi float."""
bersih = teks.replace("Rp", "").replace(" ", "").strip()
if "." in bersih and "," not in bersih:
bersih = bersih.replace(".", "")
try:
return float(bersih)
except ValueError as e:
raise TransformError(f"harga tidak valid: {teks!r}") from eKondisi "." in bersih and "," not in bersih perlu dijelaskan: titik tanpa koma dianggap pemisah ribuan (1.250.000 menjadi 1250000), bukan desimal. Bila suatu hari sumber memakai desimal koma seperti 1.250,50, kondisi ini tidak menghapus titiknya; float lalu menolak string tersebut sehingga baris masuk karantina, bukan salah dibaca sebagai 1,25.
clean_orders Baris demi Baris #
Inti transformasi berjalan dalam satu perulangan yang membaca baris mentah dan memutuskan: masuk fact, atau masuk karantina:
bersih: dict[str, dict] = {}
karantina: list[dict] = []
for posisi, (_, mentah) in enumerate(df.iterrows()):
baris_csv = posisi + 2 # baris 1 pada berkas adalah header
baris = {k: str(mentah.get(k, "")).strip() for k in df.columns}
order_id = baris.get("order_id", "")
ditolak = False
for kolom in KOLOM_WAJIB_TEKS: # (order_id, name, city, category)
if not baris.get(kolom):
karantina.append({
"baris": baris_csv, "order_id": order_id,
"alasan": f"{kolom} kosong", "nilai": "",
})
ditolak = True
try:
kuantitas = int(baris.get("quantity", ""))
except ValueError:
kuantitas = 0
if kuantitas <= 0 and not ditolak:
karantina.append({"baris": baris_csv, "order_id": order_id,
"alasan": "quantity tidak valid",
"nilai": baris.get("quantity", "")})
ditolak = True
if ditolak:
continue
bersih[order_id] = {
"order_id": order_id,
"order_date": waktu.date(),
"quantity": kuantitas,
"unit_price": round(harga_satuan, 2),
"total_price": round(kuantitas * harga_satuan, 2),
"status": baris.get("status", "").lower(),
}Mari kita telaah alurnya:
- Baris 4 (
baris_csv = posisi + 2):iterrows()memberi indeks baris data mulai nol; +2 mengubahnya menjadi nomor baris berkas yang pembaca bisa langsung cari di editor. - Baris 5: seluruh kolom dipangkas spasinya sekali di awal. Setelah baris ini,
" Budi Santoso "menjadi"Budi Santoso". - Baris 9-15: empat kolom teks wajib diperiksa kosong. Satu baris bisa menghasilkan lebih dari satu catatan karantina bila dua kolomnya kosong sekaligus.
- Baris 17-27:
quantitydicoba sebagaiint; kegagalan atau nilai tidak positif memasukkan baris ke karantina. Pemeriksaanunit_pricedanorder_timemengikuti pola yang sama (dipangkas dari potongan agar tetap mudah dibaca). - Baris 29-30 (
continue): baris yang ditolak tidak pernah menyentuh dictionarybersih. - Baris 32-40: baris valid disusun menjadi kamus fact.
total_pricedihitung di sini sebagai turunan, danstatusdinormalkan ke huruf kecil. - Penimpaan
bersih[order_id]: karena dictionary berkunciorder_id, kemunculan kedua dari pesanan yang sama menimpa kemunculan pertamanya. Efeknya: koreksi yang ditulis ulang menang.
Hasil Pembersihan Nyata #
Partisi 2026-11-03 berisi sepuluh baris mentah. Setelah clean_orders, angkanya menjadi:
Output yang Dihasilkan:
baris mentah : 10
fact bersih : 7
baris karantina : 2
duplikat dibuang : 1
== contoh baris fact hasil pembersihan ==
order_id customer_name city quantity unit_price total_price status
ORD-2026-11-03-001 Budi Santoso Jakarta 1 899000.0 899000.0 selesai
ORD-2026-11-03-002 Siti Rahmawati Bandung 1 1250000.0 1250000.0 selesai
ORD-2026-11-03-003 Andi Wijaya Yogyakarta 2 25000.0 50000.0 diproses
== daftar karantina (dead-letter sebelum ditulis) ==
baris CSV ke-8: order_id='ORD-2026-11-03-006' alasan='customer_name kosong'
baris CSV ke-11: order_id='ORD-2026-11-03-009' alasan='city kosong'
== baris ORD-2026-11-03-004 (duplikat, kemunculan terakhir menang) ==
order_id customer_name city quantity unit_price total_price status
ORD-2026-11-03-004 Dewi Lestari Surabaya 3 75000.0 225000.0 selesaiAritmetikanya tertutup: 10 baris mentah = 7 fact + 2 karantina + 1 duplikat. Baris ORD-2026-11-03-004 muncul dua kali di sumber dengan versi pertama quantity 2, status diproses; versi terakhir yang hidup di fact dengan total_price 225000. Chapter Deduplikasi Data membahas variasi kebijakan duplikat ini, sementara kebijakan “kemunculan terakhir menang” membuat pemuatan ulang aman seperti dibahas di Idempotent Transformation.
Terakhir, build_fact(clean_df, partition) menyusun kolom fact untuk tabel warehouse dan menambahkan kolom source_partition berisi tanggal partisi. Kolom inilah yang membuat rekonsiliasi pada Bagian 9 tetap tepat untuk data yang datang terlambat. Selanjutnya kita memuat tujuh baris fact ini ke DuckDB.