Tim pengawasan harga sebuah dinas perdagangan fiktif mencatat harga harian lima komoditas pangan di pasar tiga kota: Jakarta, Bandung, dan Makassar. Catatannya tersimpan sebagai satu berkas harga_komoditas.csv untuk 1 Januari sampai 31 Maret 2026. Seperti berkas lapangan pada umumnya, datanya belum rapi: ada harga tercatat dua kali, harga kelebihan satu angka nol, harga kosong, serta label kota dan satuan yang ditulis beragam. Sebelum menghitung kenaikan harga, kita perlu memuat berkas itu dengan benar dan tahu letak masalahnya. Pada bagian ini kita menyiapkan dataset, memuatnya dengan read_csv(), memeriksa strukturnya, lalu menyusun daftar masalah sebagai kontrak kerja Bagian 2.

Dataset Harga yang Dapat Direproduksi #

Dataset proyek ini dibuat oleh skrip, bukan diunduh dari internet, agar setiap pembaca bekerja pada data yang persis sama. Skrip buat_dataset.py membangun grid 90 hari x 5 komoditas x 3 kota yang berisi 1350 baris memakai np.random.default_rng(2026), lalu menyuntikkan masalah yang dirancang: 45 baris dihilangkan, enam harga salah ketik menjadi sepuluh kali lipat, sepuluh harga dikosongkan, 24 baris label kota dan 16 baris label satuan diubah bentuk penulisannya, serta delapan baris disalin sebagai duplikat. Hasil akhirnya 1313 baris mentah, pola dataset sintetis yang Anda kenal dari chapter Reproducible Data Processing.

KolomTipe di CSVIsi
tanggaltekstanggal pencatatan, 1 Januari sampai 31 Maret 2026
komoditasteks5 komoditas, misalnya Cabai Merah Keriting
kotateksJakarta, Bandung, atau Makassar
satuantekskg atau liter
hargabilangan bulatharga harian dalam rupiah per satuan

Jalankan skrip berikut untuk melihat ukuran dataset yang dihasilkan.

python
"""Generator dataset harga harian komoditas pangan (proyek Analisis Harga Komoditas).

Dataset mentah dibuat secara deterministik (seed 2026) dan sengaja memuat
lima masalah kualitas data untuk dilatih:
  1. baris duplikat,
  2. harga ekstrem (kesalahan ketik angka nol tambahan),
  3. harga kosong (NaN),
  4. label kota dan satuan tidak seragam,
  5. baris hilang (dua tanggal utuh dan lima pasangan tanggal-komoditas).
"""
from __future__ import annotations

import numpy as np
import pandas as pd

SEED = 2026
JUMLAH_HARI = 90
JUMLAH_DUPLIKAT = 8

# nama komoditas: (harga dasar, satuan, tren harian, volatilitas harian)
KOMODITAS = {
    "Beras Premium": (15500, "kg", 1.0006, 0.006),
    "Cabai Merah Keriting": (55000, "kg", 1.0000, 0.020),
    "Bawang Merah": (38000, "kg", 1.0002, 0.008),
    "Minyak Goreng Kemasan": (17500, "liter", 1.0001, 0.005),
    "Gula Pasir": (18000, "kg", 1.0005, 0.006),
}

KOTA_FAKTOR = {"Jakarta": 1.005, "Bandung": 0.980, "Makassar": 1.015}

VARIAN_KOTA = {
    "Jakarta": ["JAKARTA", "jakarta", " Jakarta "],
    "Bandung": ["BANDUNG", "bandung", " Bandung "],
    "Makassar": ["MAKASSAR", "makassar", " Makassar "],
}

VARIAN_SATUAN = {"kg": ["KG", "Kg", " kg "], "liter": ["LITER", "Liter", " liter "]}


def faktor_tren(nama: str) -> np.ndarray:
    """Faktor kumulatif harga per komoditas untuk 90 hari (indeks 0 = 1 Jan)."""
    _, _, tren, _ = KOMODITAS[nama]
    faktor = np.full(JUMLAH_HARI, tren)
    if nama == "Cabai Merah Keriting":
        faktor[32:49] = 1.035  # lonjakan pasokan 2-18 Februari
        faktor[49:64] = 0.964  # koreksi bertahap 19 Februari - 5 Maret
    return np.cumprod(faktor)


def buat_dataset() -> pd.DataFrame:
    """Menghasilkan DataFrame mentah 1313 baris dengan masalah yang dirancang."""
    rng = np.random.default_rng(SEED)
    tanggal = pd.date_range("2026-01-01", periods=JUMLAH_HARI, freq="D")

    baris = []
    for nama, (dasar, satuan, _, _) in KOMODITAS.items():
        tren_kumulatif = faktor_tren(nama)
        noise = rng.normal(0, KOMODITAS[nama][3], size=JUMLAH_HARI)
        for i in range(JUMLAH_HARI):
            for kota, faktor_kota in KOTA_FAKTOR.items():
                harga = dasar * tren_kumulatif[i] * faktor_kota * (1 + noise[i])
                baris.append((tanggal[i], nama, kota, satuan, max(1, round(harga))))
    df = pd.DataFrame(
        baris, columns=["tanggal", "komoditas", "kota", "satuan", "harga"]
    )

    # 1) baris hilang: dua tanggal utuh (30 baris) + lima pasangan
    #    tanggal-komoditas (15 baris), diambil dari tanggal interior
    indeks_hilang = rng.choice(np.arange(10, 80), size=2, replace=False)
    tanggal_hilang = tanggal[indeks_hilang]
    df = df.loc[~df["tanggal"].isin(tanggal_hilang)].reset_index(drop=True)
    hari_sisa = [i for i in range(10, 80) if tanggal[i] not in set(tanggal_hilang)]
    pilih = sorted(rng.choice(len(hari_sisa) * 5, size=5, replace=False))
    for p in pilih:
        i, j = divmod(p, 5)
        nama = list(KOMODITAS)[j]
        df = df.loc[
            ~((df["tanggal"] == tanggal[hari_sisa[i]]) & (df["komoditas"] == nama))
        ]
    df = df.reset_index(drop=True)

    # satu permutasi dipakai bersama agar baris bermasalah tidak tumpang tindih
    pilih = rng.permutation(len(df))
    iks_outlier = pilih[:6]
    iks_nan = pilih[6:16]
    iks_duplikat = pilih[16 : 16 + JUMLAH_DUPLIKAT]

    # 2) harga ekstrem: kesalahan ketik angka nol tambahan (6 baris)
    df.loc[iks_outlier, "harga"] = df.loc[iks_outlier, "harga"] * 10

    # 3) harga kosong (10 baris)
    df.loc[iks_nan, "harga"] = np.nan

    # 4a) label kota tidak seragam (8 baris per kota)
    for kota in KOTA_FAKTOR:
        baris_kota = df.index[df["kota"] == kota].to_numpy()
        iks = rng.choice(baris_kota, size=8, replace=False)
        for n, i in enumerate(iks):
            df.at[i, "kota"] = VARIAN_KOTA[kota][n % 3]

    # 4b) label satuan tidak seragam (8 baris per satuan)
    for satuan in ("kg", "liter"):
        baris_satuan = df.index[df["satuan"] == satuan].to_numpy()
        iks = rng.choice(baris_satuan, size=8, replace=False)
        for n, i in enumerate(iks):
            df.at[i, "satuan"] = VARIAN_SATUAN[satuan][n % 3]

    # 5) baris duplikat (8 salinan persis)
    df = pd.concat([df, df.loc[iks_duplikat]], ignore_index=True)

    urutan = rng.permutation(len(df))
    return df.iloc[urutan].reset_index(drop=True)


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

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

Output yang Dihasilkan:

text
harga_komoditas.csv siap: 1313 baris x 5 kolom (61937 byte)

Fungsi buat_dataset() mengembalikan DataFrame 1313 baris: 1305 kombinasi unik tanggal-komoditas-kota ditambah delapan salinan persis. Blok if __name__ == "__main__" di bagian bawah skrip menuliskan dataset ke harga_komoditas.csv di dalam tempfile.TemporaryDirectory() hanya untuk melaporkan ukurannya; folder sementara itu terhapus otomatis sehingga tidak ada berkas tertinggal di folder kerja Anda.

Memuat CSV dan Memeriksa Struktur #

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

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

import pandas as pd

from buat_dataset import buat_dataset


if __name__ == "__main__":
    with tempfile.TemporaryDirectory() as folder:
        berkas = Path(folder) / "harga_komoditas.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 harga (dibulatkan 2 desimal) ==")
    print(df["harga"].describe().round(2).to_string())

    median = df.groupby("komoditas")["harga"].transform("median")
    pasangan = df.drop_duplicates().groupby(["tanggal", "komoditas"]).size()
    print("\n== Masalah yang ditemukan ==")
    print(f"baris duplikat                         : {df.duplicated().sum()}")
    print(f"harga kosong (NaN)                     : {df['harga'].isna().sum()}")
    print(f"harga ekstrem (> 2x median komoditas)  : {(df['harga'] > 2 * median).sum()}")
    print(f"bentuk penulisan kota                  : {df['kota'].nunique()}")
    print(f"bentuk penulisan satuan                : {df['satuan'].nunique()}")
    print(f"tanggal unik                           : {df['tanggal'].nunique()} dari 90 hari")
    print(f"pasangan tanggal-komoditas lengkap     : {(pasangan == 3).sum()} dari 450")

Output yang Dihasilkan:

text
== Struktur dataset ==
bentuk: 1313 baris x 5 kolom
tanggal          str
komoditas        str
kota             str
satuan           str
harga        float64

== Lima baris pertama ==
   tanggal            komoditas     kota satuan   harga
2026-01-26 Cabai Merah Keriting Makassar     kg 57683.0
2026-01-23         Bawang Merah  Bandung     kg 37422.0
2026-03-28        Beras Premium  Jakarta     kg 16347.0
2026-02-06        Beras Premium  Jakarta     kg 15877.0
2026-02-21         Bawang Merah Makassar     kg 39479.0

== Statistik ringkas kolom harga (dibulatkan 2 desimal) ==
count      1303.00
mean      31835.04
std       27098.20
min       15044.00
25%       17318.00
50%       18432.00
75%       38864.00
max      570240.00

== Masalah yang ditemukan ==
baris duplikat                         : 8
harga kosong (NaN)                     : 10
harga ekstrem (> 2x median komoditas)  : 6
bentuk penulisan kota                  : 12
bentuk penulisan satuan                : 8
tanggal unik                           : 88 dari 90 hari
pasangan tanggal-komoditas lengkap     : 435 dari 450

Mari kita telaah bagian-bagian pentingnya:

  1. df.shape dan df.dtypes: hasilnya 1313 baris x 5 kolom. Empat kolom teks bertipe str — ini perilaku bawaan pandas 3.0, bukan lagi object seperti pandas 2.x — sementara harga terbaca float64.
  2. Tipe float64 pada harga: meskipun harga di CSV berupa bilangan bulat, sepuluh sel kosong memaksa pandas memakai float64 karena NaN hanya tersedia pada tipe float; kolom angka dengan nilai kosong selalu terbaca float64.
  3. df.head(5): urutan tanggalnya acak — 2026-01-26 lalu 2026-01-23 — bukti baris perlu sort_values() sebelum analisis deret waktu pada Bagian 3.
  4. describe(): max harga 570240.00 hampir sepuluh kali lipat mediannya 18432.00 — inilah harga ekstrem buatan kesalahan ketik. Baris count yang hanya 1303 dari 1313 mengonfirmasi sepuluh harga kosong.
  5. Deteksi harga ekstrem: transform("median") membandingkan tiap harga dengan median komoditasnya sendiri; harga di atas dua kali lipat median dihitung ekstrem, sehingga komoditas mahal seperti cabai tidak dicap sebagai outlier.

Daftar Masalah yang Terdeteksi #

Rekap di akhir output merangkum temuan dalam angka pasti:

MasalahJumlahCara deteksi
Baris duplikat8df.duplicated()
Harga kosong (NaN)10df["harga"].isna()
Harga ekstrem6pembandingan dengan median per komoditas
Label kota tidak seragam12 bentukdf["kota"].nunique()
Label satuan tidak seragam8 bentukdf["satuan"].nunique()
Baris hilang45 kombinasipasangan tanggal-komoditas lengkap hanya 435 dari 450

Enam kelompok masalah ini menjadi target kerja Bagian 2: delapan baris duplikat dihapus, 12 bentuk label kota dan 8 bentuk label satuan diseragamkan, kolom tanggal dikonversi menjadi datetime64, enam harga ekstrem dipotong ke acuan harian, dan sepuluh harga kosong diisi dari harga terakhir. Teori tiap teknik sudah dibahas pada section Data Cleaning serta section Missing Data dan Outlier Lanjutan; di proyek ini kita fokus merangkainya menjadi satu prosedur utuh.

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