Pembersihan data bukan sekadar menghapus yang terlihat janggal, melainkan memperbaiki setiap masalah dengan alasan yang bisa dipertanggungjawabkan. Pada bagian ini kita mengeksekusi rencana penanganan dari Bagian 4 satu per satu: membuang duplikat, menyeragamkan nama menu, mengubah tanda - menjadi nilai kosong yang bisa diisi, lalu memperbaiki nilai pendapatan nol. Di akhir bagian kita punya data bersih 48 baris yang lolos pemeriksaan ulang.
Urutan Pembersihan yang Dipilih #
Urutan langkah menentukan hasil. Kita kerjakan dengan urutan berikut dan alasannya:
- Buang duplikat lebih dulu — baris salinan tidak membawa informasi baru dan akan mendistorsi perhitungan apa pun yang dilakukan setelahnya.
- Seragamkan nama menu ke huruf kecil — agregasi per menu akan memisahkan
Kopi Susudarikopi susubila ini belum dibereskan. - Ubah
-menjadiNaNdan jadikan kolom gelas numerik — nilai kosong harus berbentukNaNagar pandas bisa membedakannya dari data asli. - Hitung harga satuan tiap menu dari baris yang sehat — harga satuan menjadi kunci untuk memperbaiki dua masalah tersisa.
- Perbaiki pendapatan 0 dan isi gelas yang kosong — keduanya dihitung dari harga satuan, bukan dari tebakan.
Langkah 1-3: Duplikat, Konsistensi, dan Sel Kosong #
Tiga langkah pertama dikerjakan sekali jalan karena sama-sama menyangkut bentuk baris, bukan nilainya:
import pandas as pd
df = pd.read_csv("../data/penjualan_kotor.csv")
# Langkah 1: buang baris duplikat persis.
bersih = df.drop_duplicates().reset_index(drop=True)
# Langkah 2: seragamkan nama menu ke huruf kecil.
bersih["menu"] = bersih["menu"].str.lower()
# Langkah 3: ubah '-' menjadi NaN lalu jadikan kolom gelas numerik.
bersih["gelas"] = pd.to_numeric(bersih["gelas"].replace("-", pd.NA))
print("Baris data mentah :", len(df))
print("Baris setelah bersih:", len(bersih))
print("Menu unik :", bersih["menu"].nunique())
print("Gelas masih kosong :", bersih["gelas"].isna().sum())
print("\nTipe data setelah langkah 1-3:")
print(bersih.dtypes.to_string())Output yang Dihasilkan:
Baris data mentah : 50
Baris setelah bersih: 48
Menu unik : 4
Gelas masih kosong : 3
Tipe data setelah langkah 1-3:
tanggal object
menu object
gelas float64
pendapatan_ribu int64Mari kita bedah hasilnya:
drop_duplicates(): membuang salinan dan menyisakan baris asli; jumlah baris turun dari 50 ke 48, persis ekspektasi 12 hari kali 4 menu.reset_index(drop=True)merapikan kembali penomoran baris setelah penghapusan.str.lower(): seluruh nama menu kini huruf kecil, sehinggamenu uniktinggal 4. Satu masalah selesai.replace("-", pd.NA)lalupd.to_numeric: tanda-diganti penanda kosong resmi, lalu kolom dikonversi menjadi angka. Tipe kolomgelasberubah menjadifloat64— bukan karena datanya pecahan, melainkan karenaNaNhanya bisa tinggal di kolom numerik bertipe pecahan. Tiga sel yang kosong sengaja kita biarkan dulu sebagaiNaN; mengisinya butuh harga satuan yang baru dihitung pada langkah berikutnya.
Langkah 4-5: Harga Satuan sebagai Kunci Perbaikan #
Menu di Toko Kopi Kita memiliki harga tetap per gelas, dan harga itu bisa dihitung dari baris-baris yang sehat: baris dengan gelas terisi dan pendapatan lebih dari nol. Dari sana kita perbaiki dua masalah terakhir:
import pandas as pd
df = pd.read_csv("../data/penjualan_kotor.csv")
bersih = df.drop_duplicates().reset_index(drop=True)
bersih["menu"] = bersih["menu"].str.lower()
bersih["gelas"] = pd.to_numeric(bersih["gelas"].replace("-", pd.NA))
# Langkah 4: harga satuan tiap menu dihitung dari baris yang lengkap.
lengkap = bersih[bersih["gelas"].notna() & (bersih["pendapatan_ribu"] > 0)]
harga_satuan = (
lengkap["pendapatan_ribu"] / lengkap["gelas"]
).groupby(lengkap["menu"]).median()
print("Harga satuan (ribu rupiah per gelas):")
print(harga_satuan.to_string())
# Langkah 5a: pendapatan 0 dihitung ulang dari gelas x harga satuan.
nol = bersih["pendapatan_ribu"] == 0
bersih.loc[nol, "pendapatan_ribu"] = (
bersih.loc[nol, "gelas"] * bersih.loc[nol, "menu"].map(harga_satuan)
).astype(int)
print("\nBaris pendapatan 0 setelah diperbaiki:")
print(bersih[nol].to_string(index=False))
# Langkah 5b: gelas kosong diisi dari pendapatan / harga satuan.
kosong = bersih["gelas"].isna()
bersih.loc[kosong, "gelas"] = (
bersih.loc[kosong, "pendapatan_ribu"] / bersih.loc[kosong, "menu"].map(harga_satuan)
)
bersih["gelas"] = bersih["gelas"].astype(int)
print("\nBaris gelas kosong setelah diisi:")
print(bersih[kosong].to_string(index=False))Output yang Dihasilkan:
Harga satuan (ribu rupiah per gelas):
menu
americano 18.0
kopi susu 22.0
matcha latte 25.0
teh tawar 10.0
Baris pendapatan 0 setelah diperbaiki:
tanggal menu gelas pendapatan_ribu
2026-01-12 teh tawar 17.0 170
Baris gelas kosong setelah diisi:
tanggal menu gelas pendapatan_ribu
2026-01-06 americano 16 288
2026-01-09 teh tawar 16 160
2026-01-15 matcha latte 26 650Mari kita bedah langkah-langkah kuncinya:
- Menyaring baris lengkap: syarat
gelas.notna()danpendapatan_ribu > 0memastikan harga satuan hanya dihitung dari baris yang sehat — baris bermasalah tidak ikut mencemari perhitungan. groupby(...).median(): median dipilih karena kebal terhadap nilai pencilan; dengan harga yang memang seragam, median tiap menu tepat sama dengan nilai aslinya (americano 18, kopi susu 22, matcha latte 25, teh tawar — dalam ribuan rupiah per gelas).map(harga_satuan): menerjemahkan nama menu menjadi harganya pada tiap baris. Baris teh tawar 12 Januari yang tadinya berpendapatan 0 kini bernilai 17 gelas dikali 10 ribu = 170 ribu.- Mengisi, bukan membuang: tiga baris gelas kosong memiliki nilai pendapatan yang sah, jadi membuangnya berarti membuang uang yang benar-benar masuk. Membaginya dengan harga satuan mengembalikan jumlah gelas yang wajar: 288/18 = 16, 160/10 = 16, dan 650/25 = 26. Semua hasil bagi bulat — bukti kuat bahwa pendapatan aslinya memang konsisten dengan harga satuan.
Verifikasi Ulang Data Bersih #
Pekerjaan pembersihan tidak selesai sebelum diperiksa ulang. Kita rangkum seluruh proses menjadi satu fungsi muat_bersih — bentuk yang sama akan kita pakai terus pada bagian-bagian berikutnya — lalu jalankan seluruh pemeriksaan Bagian 4 sekali lagi terhadap hasilnya:
import pandas as pd
def muat_bersih(path):
"""Muat data mentah dan terapkan seluruh langkah pembersihan."""
df = pd.read_csv(path).drop_duplicates().reset_index(drop=True)
df["menu"] = df["menu"].str.lower()
df["gelas"] = pd.to_numeric(df["gelas"].replace("-", pd.NA))
valid = df[df["gelas"].notna() & (df["pendapatan_ribu"] > 0)]
harga = (valid["pendapatan_ribu"] / valid["gelas"]).groupby(valid["menu"]).median()
nol = df["pendapatan_ribu"] == 0
df.loc[nol, "pendapatan_ribu"] = (
df.loc[nol, "gelas"] * df.loc[nol, "menu"].map(harga)
).astype(int)
kosong = df["gelas"].isna()
df.loc[kosong, "gelas"] = (
df.loc[kosong, "pendapatan_ribu"] / df.loc[kosong, "menu"].map(harga)
)
df["gelas"] = df["gelas"].astype(int)
return df
bersih = muat_bersih("../data/penjualan_kotor.csv")
print("Ukuran data bersih :", bersih.shape)
print("Duplikat tersisa :", bersih.duplicated().sum())
print("Gelas kosong :", bersih["gelas"].isna().sum())
print("Pendapatan 0 :", (bersih["pendapatan_ribu"] == 0).sum())
print("Menu unik :", sorted(bersih["menu"].unique()))
print("Rentang tanggal :", bersih["tanggal"].min(), "s.d.", bersih["tanggal"].max())
print("Total pendapatan :", bersih["pendapatan_ribu"].sum(), "ribu rupiah")Output yang Dihasilkan:
Ukuran data bersih : (48, 4)
Duplikat tersisa : 0
Gelas kosong : 0
Pendapatan 0 : 0
Menu unik : ['americano', 'kopi susu', 'matcha latte', 'teh tawar']
Rentang tanggal : 2026-01-05 s.d. 2026-01-16
Total pendapatan : 23649 ribu rupiahMari kita baca hasil verifikasi tersebut:
- Seluruh pemeriksaan nol: tidak ada duplikat, tidak ada gelas kosong, tidak ada pendapatan nol, dan menu unik tepat empat. Ukuran
(48, 4)sesuai ekspektasi dari awal. - Rentang tanggal utuh: 5 sampai 16 Januari 2026 — tidak ada hari yang hilang selama pembersihan, jadi kita tidak sengaja membuang hari data yang sah.
- Total pendapatan 23.649 ribu rupiah (sekitar Rp23,6 juta): angka ini menjadi tolok ukur pada bagian-bagian berikutnya. Berkat fungsi
muat_bersih, angka yang sama akan muncul setiap kali data bersih dimuat ulang — inilah arti pembersihan yang bisa direproduksi.
Data bersih sudah berdiri di atas fondasi yang teruji. Sekarang saatnya bertanya: apa cerita yang tersimpan di dalamnya? Kita mulai menjelajah dengan statistik deskriptif dan agregasi pada Bagian 6: Eksplorasi Data .