Hari 5 — Data Cleaning: Missing Values, Duplicate, Dtype, String Manipulation
Jumat, 2 jam. Setup & dataset: lihat 00_overview.md. Dataset khusus: data/sales_raw_dirty.csv.
Tujuan Belajar
- Mendeteksi missing value dan memilih strategi penanganan yang tepat (bukan asal
dropna()/fillna()) - Mendeteksi & menangani baris duplikat
- Membersihkan kolom numerik/tanggal yang formatnya tidak konsisten, lalu konversi tipe data dengan benar
- Membersihkan teks (
whitespace, casing) dengan.straccessor - Memahami kenapa parsing tanggal ambigu itu berbahaya, dan cara menghindarinya
Untuk Instruktur: Mindset Shift Paling Penting Minggu Ini
Ini beda secara filosofis dari 4 hari sebelumnya. Hari 1–4 soal “bagaimana cara menulis kode yang benar”. Hari ini soal “apa keputusan yang benar saat data-nya sendiri tidak lengkap/tidak konsisten” — dan itu sering kali bukan keputusan teknis, tapi keputusan bisnis yang harus dikonfirmasi.
Developer yang terbiasa membangun aplikasi biasanya punya refleks: input tidak valid → tolak / lempar exception. Di data engineering, refleks itu tidak selalu tepat — data yang “kotor” adalah kondisi normal, bukan kondisi eksepsional, karena datanya berasal dari sistem lain (kadang legacy, kadang input manual manusia) yang tidak bisa kamu kontrol. Tugas data engineer bukan menolak data kotor, tapi punya strategi eksplisit dan terdokumentasi untuk tiap jenis masalah: dibuang? diperbaiki (dan dengan asumsi apa)? dibiarkan tapi ditandai untuk dicek manual? Ini akan jadi pola pikir utama di mini project akhir pekan ini juga.
Konsep & Sintaks
1. Deteksi & Strategi Missing Value
df.isna().sum() # jumlah nilai NaN per kolom — langkah pertama selalu
df.info() # cara cepat lain lihat non-null count per kolom
Strategi (pilih sesuai konteks, bukan template baku):
df.dropna() # buang baris yang ADA NaN di kolom manapun — agresif
df.dropna(subset=["customer_name"]) # buang baris HANYA kalau customer_name kosong — lebih presisi
df.fillna({"category": "Unknown"}) # isi dengan nilai default eksplisit
df["quantity"].fillna(df["quantity"].median()) # isi dengan statistik (median lebih tahan outlier dari mean)
2. Deteksi & Penanganan Duplicate
df.duplicated() # boolean Series, True untuk baris yang identik dengan baris SEBELUMNYA
df.duplicated().sum() # jumlah baris duplikat
df[df.duplicated()] # lihat baris mana saja yang duplikat, sebelum dibuang
df.drop_duplicates() # buang duplikat (simpan kemunculan pertama by default)
Selalu lihat dulu baris yang keteridentifikasi duplikat sebelum drop_duplicates() — supaya yakin itu betul duplikat data (bukan 2 transaksi berbeda yang kebetulan datanya identik).
3. Membersihkan & Konversi Tipe Data
pd.to_numeric(series, errors="coerce") # ubah ke angka, yang gagal parse jadi NaN (bukan error)
pd.to_datetime(series, errors="coerce") # ubah ke datetime, yang gagal parse jadi NaT
errors="coerce" itu pilihan sadar: kamu memilih kehilangan baris yang gagal parse (jadi NaN/NaT) daripada program crash. Setelah itu, wajib cek berapa banyak yang jadi NaN/NaT — itu petunjuk seberapa “kotor” data sumbernya.
4. String Cleaning dengan .str
df["country"].str.strip() # buang whitespace di awal/akhir
df["country"].str.upper() # normalisasi casing (lihat catatan di Kesalahan Umum soal .title())
df["unit_price"].str.replace("$", "", regex=False)
Contoh Kode — Membersihkan sales_raw_dirty.csv Langkah demi Langkah
import pandas as pd
df = pd.read_csv("data/sales_raw_dirty.csv")
df.info()
Output .info() akan menunjukkan: 20 baris, tapi customer_name cuma 19 non-null, category cuma 18 non-null, quantity cuma 19 non-null (dan sudah ke-cast jadi float64, bukan int64, karena ada NaN — Pandas otomatis “menaikkan” tipe int ke float begitu ada nilai kosong di kolom itu). unit_price bertipe object (string), bukan angka — karena isinya campuran "15.00", "$8.00", "3,50".
Langkah 1 — cek & tangani missing value
print(df.isna().sum())
# customer_name: 1, category: 2, quantity: 1
df["category"] = df["category"].fillna("Unknown")
df["quantity"] = df["quantity"].fillna(df["quantity"].median())
df = df.dropna(subset=["customer_name"]) # baris tanpa nama customer tidak berguna untuk analisis per-customer
Langkah 2 — cek & buang duplikat
print(df.duplicated().sum()) # 1
df = df.drop_duplicates()
Langkah 3 — bersihkan country (whitespace + casing tidak konsisten)
print(df["country"].unique())
# [' indonesia', 'Indonesia', 'UK ', 'UK', 'Germany', 'Singapore', 'INDONESIA']
df["country"] = df["country"].str.strip().str.upper()
print(df["country"].unique())
# ['INDONESIA', 'UK', 'GERMANY', 'SINGAPORE']
Langkah 4 — bersihkan unit_price (simbol mata uang & pemisah desimal campuran)
df["unit_price"] = (
df["unit_price"].astype(str)
.str.replace("$", "", regex=False)
.str.replace(",", ".", regex=False)
)
df["unit_price"] = pd.to_numeric(df["unit_price"], errors="coerce")
Langkah 5 — parsing tanggal, dengan penuh kewaspadaan (lihat Kesalahan Umum #2 sebelum coba shortcut apapun)
df["order_date_parsed"] = pd.to_datetime(df["order_date"], format="%Y-%m-%d", errors="coerce")
gagal = df[df["order_date_parsed"].isna()]
print(gagal[["order_id", "order_date"]])
# order_id 10, order_date '12/03/2024' — satu-satunya yang gagal parse format standar
Kesalahan Umum
-
Percaya begitu saja tipe data hasil
read_csv(). Kolomunit_pricedi dataset ini terbaca sebagaiobject(string), bukan angka — karena campuran"$8.00"dan"3,50". Kalau langsung dipakai untuk operasi matematika tanpa cekdtypesdulu, hasilnya bisa error (TypeError) atau, lebih berbahaya, operasi string yang “kebetulan jalan” tapi hasilnya salah (mis."15.00" * 2menghasilkan"15.0015.00", bukan30.0, karena Python mengalikan string, bukan angka). Selaludf.dtypesataudf.info()di awal, jangan asumsi. - Mempercayai
pd.to_datetime()menebak format tanggal dengan benar untuk tanggal ambigu. Ini bug paling berbahaya di sesi ini karena tidak menghasilkan error — hasilnya salah secara diam-diam.# BAHAYA: pd.to_datetime(df["order_date"], format="mixed") # '12/03/2024' ditebak sebagai 3 Desember 2024, PADAHAL baris lain untuk order_id yang sama # menunjukkan '2024-03-12' (12 Maret) — keduanya order_id 10, tanggalnya harus sama!12/03/2024bisa berarti 12 Maret (formatDD/MM/YYYY, umum di Indonesia/Eropa) atau 3 Desember (formatMM/DD/YYYY, umum di Amerika) — keduanya valid secara sintaks, dan pandas akan menebak salah satu tanpa memberitahu kamu bahwa itu tebakan. Pendekatan yang aman: jangan pernah andalkan inference otomatis untuk tanggal dari sumber yang formatnya tidak kamu ketahui pasti. Parsing eksplisit dengan format yang diketahui benar, isolasi baris yang gagal parse (errors="coerce"lalu cek.isna()), lalu investigasi baris-baris itu satu per satu — seperti dicontohkan di Langkah 5 di atas. -
.str.title()merusak akronim."UK".str.title()menghasilkan"Uk", bukan"UK"— title-case mengasumsikan setiap kata adalah kata biasa, bukan singkatan. Untuk normalisasi kategori/kode (negara, status, dsb.),.str.upper()atau.str.lower()jauh lebih aman daripada.str.title()..str.title()baru masuk akal untuk data yang memang nama orang/tempat biasa ("andi wijaya"→"Andi Wijaya"), bukan kode/singkatan. -
dropna()tanpasubset=terlalu agresif.df.dropna()polos akan membuang baris kalau kolom manapun ada yangNaN— di dataset ini efeknya membuang baris yang cumacategory-nya kosong padahal datanya sendiri (customer, produk, harga) lengkap dan berguna. Selalu spesifik:dropna(subset=[...])untuk kolom yang benar-benar wajib ada isinya. - Asumsi
quantitynegatif itu data error, padahal itu sinyal bisnis. Barisorder_id=11(Emma Watson, refunded) punyaquantity = -1— ini bukan data kotor yang perlu “diperbaiki” jadi positif, tapi konvensi umum di data retail: quantity negatif = barang dikembalikan/retur. Ini pola yang sama persis dengan yang akan ditemui di dataset Online Retail II minggu ini (lihatlatihan_eda_dan_mini_project.md) — jangan “bersihkan” nilai yang sebetulnya bermakna.
Latihan
Pakai data/sales_raw_dirty.csv.
- Hitung jumlah
NaNper kolom, lalu tentukan strategi untuk masing-masing (drop baris / isi default / isi statistik) — tulis alasan tiap keputusan dalam 1 kalimat. - Bersihkan kolom
country(whitespace + casing), lalu tampilkanvalue_counts()-nya setelah dibersihkan. - Bersihkan kolom
unit_pricejadi numerik sepenuhnya (tangani$dan koma desimal), lalu hitungsubtotal = quantity * unit_priceuntuk tiap baris. - Deteksi baris yang gagal di-parse
pd.to_datetime(..., format="%Y-%m-%d", errors="coerce"), investigasi manual (cross-check ke baris lain denganorder_idyang sama kalau ada), lalu perbaiki. - Setelah semua langkah cleaning selesai (gabungkan #1–#4), hitung total revenue (
sum(subtotal)untukstatus == 'completed') dari data yang sudah bersih — bandingkan dengan angka807.00yang sudah diverifikasi berkali-kali dimateri/minggu_1/untuk dataset yang sama dalam kondisi bersih. Kalau angkanya beda, itu tandanya ada langkah cleaning yang belum tepat — cari selisihnya.
Kunci Jawaban & Pembahasan
1.
df.isna().sum()
customer_name(1 baris kosong): drop baris ini — tanpa identitas customer, baris ini tidak berguna untuk analisis per-customer yang jadi fokus mini project.category(2 baris kosong): isi"Unknown"— baris tetap berguna untuk analisis revenue total, cuma tidak bisa masuk breakdown per kategori; membuang baris ini artinya kehilangan data penjualan asli tanpa alasan kuat.quantity(1 baris kosong): isi dengan median — dianggap lebih aman daripada0(yang akan salah membuat subtotal jadi 0, padahal transaksinya nyata) atau mean (lebih rentan bias dari outlier seperti order dengan quantity 10).
2.
df["country"] = df["country"].str.strip().str.upper()
df["country"].value_counts()
Hasil: INDONESIA (paling banyak), UK, GERMANY, SINGAPORE — total distinct values turun dari 7 varian kotor jadi 4 nilai bersih.
3.
df["unit_price"] = (
df["unit_price"].astype(str)
.str.replace("$", "", regex=False)
.str.replace(",", ".", regex=False)
)
df["unit_price"] = pd.to_numeric(df["unit_price"], errors="coerce")
df["subtotal"] = df["quantity"] * df["unit_price"]
4.
df["order_date_parsed"] = pd.to_datetime(df["order_date"], format="%Y-%m-%d", errors="coerce")
gagal = df[df["order_date_parsed"].isna()]
print(gagal) # order_id 10, '12/03/2024'
Cross-check: baris lain dengan order_id == 10 punya order_date = '2024-03-12' (12 Maret) — jadi '12/03/2024' seharusnya juga 12 Maret, bukan 3 Desember. Perbaikan manual:
df.loc[df["order_date"] == "12/03/2024", "order_date_parsed"] = pd.Timestamp("2024-03-12")
Ini contoh nyata kenapa Langkah 5 di atas (isolasi baris gagal, investigasi manual) jauh lebih aman daripada mempercayai pd.to_datetime(..., format="mixed") begitu saja — kalau dibiarkan, tanggal order ini akan salah 3 bulan lebih tanpa ada error apapun yang muncul.
5. Setelah semua langkah #1–#4 diterapkan dengan benar (termasuk membuang 1 baris duplikat, dan dropna(subset=["customer_name"])), filter status == 'completed' lalu sum(subtotal) menghasilkan 807.00 — cocok persis dengan angka yang sudah diverifikasi berkali-kali di materi/minggu_1/ untuk dataset bersihnya.
Menariknya, kecocokan ini sedikit “kebetulan”: baris order_id=2 (Mechanical Keyboard) dibuang karena customer_name-nya kosong (kehilangan revenue 45.00), sementara baris order_id=5 (Mechanical Keyboard juga) yang quantity-nya kosong diisi median (2.0) — padahal nilai aslinya (bisa dicek di data/order_items.csv) adalah 1, jadi revenue baris itu jadi kelebihan 45.00 (90.00 alih-alih 45.00 yang seharusnya). Dua penyimpangan ini (−45.00 dan +45.00) kebetulan saling menutupi. Poin pentingnya: strategi cleaning yang berbeda (mis. dropna() untuk quantity alih-alih diisi median) akan menghasilkan total akhir yang berbeda dari 807.00 — kecocokan sempurna di atas bukan jaminan, cuma kebetulan dari data spesifik ini. Yang harus dipegang: keputusan cleaning didokumentasikan & masuk akal, bukan dikejar supaya “pas” dengan angka tertentu.