Anda baru saja menerima tugas menganalisis kinerja penjualan Toko Sembako Berkah, jaringan toko sembako fiktif dengan empat cabang di Jakarta, Bandung, Surabaya, dan Yogyakarta. Sumber datanya satu berkas penjualan.csv hasil ekspor sistem kasir, berisi transaksi Januari hingga Maret 2025. Seperti berkas ekspor pada umumnya, datanya tidak langsung rapi: ada baris ganda, harga yang tidak masuk akal, kolom kategori yang kosong, dan tanggal yang ditulis dengan dua format berbeda. Sebelum membersihkan apa pun, kita perlu tahu dengan pasti apa isi berkas tersebut dan di mana letak masalahnya. Pada bagian ini kita menyiapkan dataset latihan, memuatnya dengan pandas, memeriksa strukturnya, lalu menyusun daftar masalah sebagai kontrak kerja Bagian 2.

Dataset Latihan yang Dapat Direproduksi #

Dataset proyek ini dibuat oleh skrip, bukan diunduh dari internet, agar semua pembaca bekerja pada data yang sama. Skrip buat_dataset.py membangun 240 transaksi dasar memakai np.random.default_rng(42) sehingga hasilnya identik di komputer mana pun, lalu menambahkan enam salinan baris sebagai duplikat. Pola ini mengikuti chapter Reproducible Data Processing pada section Pengujian dan Debugging.

KolomTipe di CSVIsi
id_transaksitekskode TRX-0001 sampai TRX-0240
tanggaltekstanggal transaksi, 1 Januari sampai 31 Maret 2025
produkteks10 produk sembako, misalnya Beras Premium 5kg
kategoriteksBahan Pokok, Minuman, Perawatan, atau Kebersihan
kotatekscabang: Jakarta, Bandung, Surabaya, Yogyakarta
jumlahbilangan bulatbanyak unit per transaksi (1 sampai 10)
hargabilangan bulatharga satuan dalam rupiah

Skrip yang sama juga menyediakan fungsi buat_modal() berisi harga beli tiap produk; tabel kecil ini baru dipakai pada Bagian 3. Jalankan skrip berikut untuk melihat ukuran dataset yang dihasilkan.

python
"""Generator dataset transaksi penjualan Toko Sembako Berkah.

Dataset mentah dibuat secara deterministik (seed 42) dan sengaja memuat
beberapa masalah kualitas data untuk dilatih:
  1. label kategori tidak seragam (kapitalisasi dan spasi),
  2. harga 0 atau negatif,
  3. kategori kosong (NaN),
  4. format tanggal campur (ISO dan DD/MM/YYYY),
  5. baris duplikat.
"""
from __future__ import annotations

import numpy as np
import pandas as pd

SEED = 42
JUMLAH_BARIS = 240
JUMLAH_DUPLIKAT = 6

PRODUK_HARGA = {
    "Beras Premium 5kg": 68000,
    "Gula Pasir 1kg": 14500,
    "Minyak Goreng 2L": 33000,
    "Tepung Terigu 1kg": 12000,
    "Kopi Bubuk 200g": 24000,
    "Teh Celup Isi 50": 9500,
    "Susu Kental Manis": 11500,
    "Sabun Mandi Batang": 4500,
    "Shampoo Sachet": 3000,
    "Deterjen Bubuk 800g": 18500,
}

PRODUK_MODAL = {
    "Beras Premium 5kg": 52000,
    "Gula Pasir 1kg": 11000,
    "Minyak Goreng 2L": 26000,
    "Tepung Terigu 1kg": 9000,
    "Kopi Bubuk 200g": 17500,
    "Teh Celup Isi 50": 7000,
    "Susu Kental Manis": 8800,
    "Sabun Mandi Batang": 3200,
    "Shampoo Sachet": 2100,
    "Deterjen Bubuk 800g": 13500,
}

PRODUK_KATEGORI = {
    "Beras Premium 5kg": "Bahan Pokok",
    "Gula Pasir 1kg": "Bahan Pokok",
    "Minyak Goreng 2L": "Bahan Pokok",
    "Tepung Terigu 1kg": "Bahan Pokok",
    "Kopi Bubuk 200g": "Minuman",
    "Teh Celup Isi 50": "Minuman",
    "Susu Kental Manis": "Minuman",
    "Sabun Mandi Batang": "Perawatan",
    "Shampoo Sachet": "Perawatan",
    "Deterjen Bubuk 800g": "Kebersihan",
}

KOTA_CABANG = ["Jakarta", "Bandung", "Surabaya", "Yogyakarta"]

VARIAN_LABEL = {
    "Bahan Pokok": ["bahan pokok", "BAHAN POKOK", " Bahan Pokok "],
    "Minuman": ["minuman", "MINUMAN", " Minuman "],
    "Perawatan": ["perawatan", "PERAWATAN", " Perawatan "],
    "Kebersihan": ["kebersihan", "KEBERSIHAN", " Kebersihan "],
}


def buat_dataset() -> pd.DataFrame:
    """Menghasilkan DataFrame mentah 246 baris dengan masalah yang dirancang."""
    rng = np.random.default_rng(SEED)
    n = JUMLAH_BARIS

    produk = rng.choice(list(PRODUK_HARGA), size=n)
    tanggal = pd.Series(
        pd.Timestamp("2025-01-01")
        + pd.to_timedelta(rng.integers(0, 90, size=n), unit="D")
    )

    df = pd.DataFrame({
        "id_transaksi": [f"TRX-{i + 1:04d}" for i in range(n)],
        "tanggal": tanggal.dt.strftime("%Y-%m-%d"),
        "produk": produk,
        "kategori": pd.Series(produk).map(PRODUK_KATEGORI).to_numpy(),
        "kota": rng.choice(KOTA_CABANG, size=n),
        "jumlah": rng.integers(1, 11, size=n),
        "harga": pd.Series(produk).map(PRODUK_HARGA).to_numpy(dtype="int64"),
    })

    # 1) label kategori tidak seragam (30 baris)
    iks = rng.choice(n, size=30, replace=False)
    for i, label in zip(iks, df.loc[iks, "kategori"]):
        varian = VARIAN_LABEL[label]
        df.loc[i, "kategori"] = varian[int(rng.integers(0, len(varian)))]

    # 2) harga tidak valid: 0 atau negatif (6 baris)
    iks = rng.choice(n, size=6, replace=False)
    df.loc[iks[:4], "harga"] = 0
    df.loc[iks[4:], "harga"] = -df.loc[iks[4:], "harga"]

    # 3) kategori kosong (8 baris)
    iks = rng.choice(n, size=8, replace=False)
    df.loc[iks, "kategori"] = np.nan

    # 4) format tanggal campur (36 baris, hanya hari 13-31 agar tidak ambigu)
    kandidat = tanggal.index[tanggal.dt.day > 12].to_numpy()
    iks = rng.choice(kandidat, size=36, replace=False)
    df.loc[iks, "tanggal"] = tanggal.loc[iks].dt.strftime("%d/%m/%Y").to_numpy()

    # 5) baris duplikat (6 baris salinan persis)
    iks = rng.choice(n, size=JUMLAH_DUPLIKAT, replace=False)
    df = pd.concat([df, df.loc[iks]], ignore_index=True)

    # acak urutan baris agar masalah tersebar, lalu rapikan index
    urutan = rng.permutation(len(df))
    return df.iloc[urutan].reset_index(drop=True)


def buat_modal() -> pd.DataFrame:
    """Tabel harga modal (harga beli) per produk untuk latihan merge."""
    return pd.DataFrame({
        "produk": list(PRODUK_MODAL),
        "harga_modal": list(PRODUK_MODAL.values()),
    })


if __name__ == "__main__":
    import tempfile
    from pathlib import Path

    df = buat_dataset()
    with tempfile.TemporaryDirectory() as folder:
        berkas = Path(folder) / "penjualan.csv"
        df.to_csv(berkas, index=False)
        print(
            f"penjualan.csv siap: {df.shape[0]} baris x {df.shape[1]} kolom "
            f"({berkas.stat().st_size} byte)"
        )

Output yang Dihasilkan:

text
penjualan.csv siap: 246 baris x 7 kolom (15797 byte)

Fungsi buat_dataset() mengembalikan DataFrame 246 baris: 240 transaksi asli ditambah 6 salinan persis. Bagian paling bawah skrip, blok if __name__ == "__main__", menuliskan dataset ke penjualan.csv di dalam tempfile.TemporaryDirectory() hanya untuk melaporkan ukurannya; folder sementara itu dihapus otomatis sehingga tidak ada berkas yang tertinggal.

Memuat CSV dan Memeriksa Struktur #

Sekarang kita memperlakukan dataset seperti berkas kasir sungguhan: tulis ke CSV, muat ulang dengan read_csv(), lalu periksa strukturnya. Skrip berikut mencetak bentuk data, tipe tiap kolom, lima baris pertama, statistik ringkas, dan rekap masalah yang berhasil dideteksi.

python
"""Memuat penjualan.csv lalu memeriksa struktur dan masalah awal dataset."""
from pathlib import Path
import tempfile

import pandas as pd

from buat_dataset import PRODUK_KATEGORI, buat_dataset


if __name__ == "__main__":
    with tempfile.TemporaryDirectory() as folder:
        berkas = Path(folder) / "penjualan.csv"
        buat_dataset().to_csv(berkas, index=False)
        df = pd.read_csv(berkas)

    print("== Struktur dataset ==")
    print(f"bentuk: {df.shape[0]} baris x {df.shape[1]} kolom")
    print(df.dtypes.to_string())

    print("\n== Lima baris pertama ==")
    print(df.head(5).to_string(index=False))

    print("\n== Statistik ringkas kolom angka (dibulatkan 2 desimal) ==")
    print(df[["jumlah", "harga"]].describe().round(2).to_string())

    peta = df["produk"].map(PRODUK_KATEGORI)
    tidak_seragam = (df["kategori"].notna() & (df["kategori"] != peta)).sum()
    print("\n== Masalah yang ditemukan ==")
    print(f"baris duplikat              : {df.duplicated().sum()}")
    print(f"harga 0 atau negatif        : {(df['harga'] <= 0).sum()}")
    print(f"kategori kosong (NaN)       : {df['kategori'].isna().sum()}")
    print(f"tanggal format DD/MM/YYYY   : {df['tanggal'].str.contains('/').sum()}")
    print(
        f"label kategori tidak seragam: {tidak_seragam} baris, "
        f"{df['kategori'].dropna().nunique()} bentuk penulisan"
    )

Output yang Dihasilkan:

text
== Struktur dataset ==
bentuk: 246 baris x 7 kolom
id_transaksi      str
tanggal           str
produk            str
kategori          str
kota              str
jumlah          int64
harga           int64

== Lima baris pertama ==
id_transaksi    tanggal            produk    kategori    kota  jumlah  harga
    TRX-0164 2025-03-29   Kopi Bubuk 200g     Minuman Bandung       2  24000
    TRX-0190 2025-03-06 Tepung Terigu 1kg Bahan Pokok Jakarta       1  12000
    TRX-0048 2025-02-21    Shampoo Sachet   Perawatan Jakarta       5   3000
    TRX-0238 2025-01-02   Kopi Bubuk 200g     Minuman Bandung       7      0
    TRX-0017 2025-03-02  Teh Celup Isi 50     Minuman Jakarta       8   9500

== Statistik ringkas kolom angka (dibulatkan 2 desimal) ==
       jumlah     harga
count  246.00    246.00
mean     5.46  18239.84
std      3.01  18302.10
min      1.00 -68000.00
25%      2.25   4500.00
50%      6.00  12000.00
75%      8.00  24000.00
max     10.00  68000.00

== Masalah yang ditemukan ==
baris duplikat              : 6
harga 0 atau negatif        : 6
kategori kosong (NaN)       : 8
tanggal format DD/MM/YYYY   : 37
label kategori tidak seragam: 26 baris, 14 bentuk penulisan

Mari kita telaah bagian-bagian pentingnya:

  1. Blok with tempfile.TemporaryDirectory(): dataset ditulis ke penjualan.csv di folder sementara lalu dimuat ulang dengan pd.read_csv(); pada pekerjaan nyata argumennya cukup diganti lokasi berkas Anda.
  2. df.shape dan df.dtypes: hasilnya 246 baris x 7 kolom. Perhatikan bahwa lima kolom teks bertipe str — ini perilaku bawaan pandas 3.0, bukan lagi object seperti pandas 2.x — sementara jumlah dan harga terbaca int64.
  3. df.head(5): lima baris pertama sudah memperlihatkan bukti masalah, yaitu transaksi TRX-0238 dengan harga 0.
  4. describe(): ringkasan statistik memperlihatkan min harga -68000.00 — harga negatif jelas tidak bermakna — sedangkan rentang jumlah 1 sampai 10 masih wajar.
  5. Pembandingan dengan PRODUK_KATEGORI: karena satu produk selalu satu kategori, nilai kategori yang berbeda dari peta produk ke kategori dihitung sebagai penulisan yang tidak seragam.

Daftar Masalah yang Terdeteksi #

Rekap di akhir output merangkum temuan dalam angka yang pasti:

MasalahJumlahCara deteksi
Baris duplikat6df.duplicated()
Harga 0 atau negatif6(df["harga"] <= 0).sum()
Kategori kosong (NaN)8df["kategori"].isna()
Tanggal format DD/MM/YYYY37df["tanggal"].str.contains("/")
Label kategori tidak seragam26 baris, 14 bentukpembandingan dengan peta produk

Lima masalah ini menjadi target kerja Bagian 2: enam baris duplikat dihapus, enam harga tidak valid dibuang, delapan kategori kosong diisi, 37 tanggal format campur diubah menjadi datetime64, dan 14 bentuk penulisan kategori diseragamkan. Teori masing-masing teknik sudah dibahas pada section Data Cleaning — antara lain chapter Menangani Nilai Duplikat, Mengisi Nilai Kosong, Memperbaiki Tipe Data, dan Standardisasi Label Kategori — sehingga di proyek ini kita fokus merangkainya menjadi satu prosedur utuh.

Sumber bacaan resmi: pandas.read_csv , pandas.DataFrame.duplicated , dan pandas.DataFrame.describe .