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:

  1. Buang duplikat lebih dulu — baris salinan tidak membawa informasi baru dan akan mendistorsi perhitungan apa pun yang dilakukan setelahnya.
  2. Seragamkan nama menu ke huruf kecil — agregasi per menu akan memisahkan Kopi Susu dari kopi susu bila ini belum dibereskan.
  3. Ubah - menjadi NaN dan jadikan kolom gelas numerik — nilai kosong harus berbentuk NaN agar pandas bisa membedakannya dari data asli.
  4. Hitung harga satuan tiap menu dari baris yang sehat — harga satuan menjadi kunci untuk memperbaiki dua masalah tersisa.
  5. 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:

python
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:

text
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      int64

Mari kita bedah hasilnya:

  1. 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.
  2. str.lower(): seluruh nama menu kini huruf kecil, sehingga menu unik tinggal 4. Satu masalah selesai.
  3. replace("-", pd.NA) lalu pd.to_numeric: tanda - diganti penanda kosong resmi, lalu kolom dikonversi menjadi angka. Tipe kolom gelas berubah menjadi float64 — bukan karena datanya pecahan, melainkan karena NaN hanya bisa tinggal di kolom numerik bertipe pecahan. Tiga sel yang kosong sengaja kita biarkan dulu sebagai NaN; 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:

python
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:

text
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              650

Mari kita bedah langkah-langkah kuncinya:

  1. Menyaring baris lengkap: syarat gelas.notna() dan pendapatan_ribu > 0 memastikan harga satuan hanya dihitung dari baris yang sehat — baris bermasalah tidak ikut mencemari perhitungan.
  2. 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).
  3. 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.
  4. 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:

python
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:

text
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 rupiah

Mari kita baca hasil verifikasi tersebut:

  1. 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.
  2. Rentang tanggal utuh: 5 sampai 16 Januari 2026 — tidak ada hari yang hilang selama pembersihan, jadi kita tidak sengaja membuang hari data yang sah.
  3. 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 .