Jaringan Toko Roti Manis menjual roti dan kue dari tiga outlet: Cihampelas di Bandung, Kemang di Jakarta, dan Malioboro di Yogyakarta. Setiap malam, sistem kasir tiap outlet mengirim satu baris rekap berisi tanggal, outlet, unit terjual, dan omzet harian. Rekap gabungan April sampai September 2026 berisi 546 baris yang belum siap dianalisis: ada baris ganda, tanggal bolong, omzet nol atau negatif, dan nilai kosong. Bagian pertama proyek ini memuat berkas tersebut, menjadikan kolom tanggal sebagai DatetimeIndex, dan menutup semua celah kalender hingga tersisa deret harian bersih 549 baris untuk tiga bagian berikutnya.

Dataset Latihan yang Dapat Direproduksi #

Dataset proyek dibuat oleh skrip, bukan diunduh dari internet, agar setiap pembaca mengolah angka yang sama persis. Skrip buat_dataset.py merakit deret harian 3 outlet x 183 hari (1 April sampai 30 September 2026) memakai np.random.default_rng(19): dasar unit harian berbeda per outlet (Kemang 380, Cihampelas 315, Malioboro 265), tren enam bulan berbeda arah (unit Kemang naik 30%, Cihampelas naik 8%, Malioboro turun 14%), bonus akhir pekan hingga 30%, dan derau acak terkontrol. Di atas deret dasar itu, skrip menanam empat kelompok masalah yang akan kita bersihkan.

KolomTipe di CSVIsi
tanggalteks ISO1 April sampai 30 September 2026, bisa ganda atau bolong
outletteksCihampelas, Kemang, atau Malioboro
unit_terjualbilangan bulatbanyak roti dan kue terjual dalam sehari
omzetbilangan bulatpenjualan harian dalam rupiah

Jalankan skrip berikut untuk melihat ukuran dataset mentah yang dihasilkan.

python
"""Generator + pemuat dataset penjualan harian Toko Roti Manis (3 outlet).

Deret waktu harian April-September 2026 (183 hari) dibuat secara
deterministik (seed 19) dan sengaja ditanami masalah yang lazim pada
rekap penjualan harian:
  1. baris ganda (duplikat persis),
  2. tanggal hilang (outlet tidak merekam),
  3. omzet 0 atau negatif (salah entri),
  4. nilai kosong (NaN) pada unit_terjual dan omzet.
"""
from __future__ import annotations

import numpy as np
import pandas as pd

SEED = 19
MULAI = pd.Timestamp("2026-04-01")
AKHIR = pd.Timestamp("2026-09-30")
JUMLAH_HARI = (AKHIR - MULAI).days + 1  # 183

OUTLET = ["Cihampelas", "Kemang", "Malioboro"]

# dasar unit terjual per hari, arah tren (perubahan unit hari ke-183
# relatif terhadap hari pertama), dan harga rata-rata per unit
UNIT_DASAR = {"Kemang": 380, "Cihampelas": 315, "Malioboro": 265}
TREN_UNIT = {"Kemang": 0.30, "Cihampelas": 0.08, "Malioboro": -0.14}
HARGA_RATA = {"Kemang": 10500, "Cihampelas": 9800, "Malioboro": 9200}

# kenaikan penjualan akhir pekan: Senin-Selasa datar, Rabu mulai naik
BONUS_DOW = [0.00, 0.00, 0.03, 0.06, 0.14, 0.30, 0.22]

JUMLAH_DUPLIKAT = 8
HILANG_PER_OUTLET = {"Cihampelas": 4, "Kemang": 3, "Malioboro": 4}


def pilih_koordinat(rng, jumlah, terpakai):
    """Memilih `jumlah` pasangan (outlet, hari) unik di luar tepi periode."""
    interior = np.arange(1, JUMLAH_HARI - 1)
    dipilih = []
    while len(dipilih) < jumlah:
        o = int(rng.integers(0, len(OUTLET)))
        h = int(rng.choice(interior))
        if (o, h) in terpakai or (o, h) in dipilih:
            continue
        dipilih.append((o, h))
    return dipilih


def pilih_hilang(rng, terpakai):
    """Tanggal hilang dengan kuota per outlet agar sebarannya merata."""
    interior = np.arange(1, JUMLAH_HARI - 1)
    dipilih = []
    for nama in OUTLET:
        io = OUTLET.index(nama)
        diambil = 0
        while diambil < HILANG_PER_OUTLET[nama]:
            h = int(rng.choice(interior))
            if (io, h) in terpakai or (io, h) in dipilih:
                continue
            dipilih.append((io, h))
            diambil += 1
    return dipilih


def buat_mentah() -> pd.DataFrame:
    """Menghasilkan DataFrame mentah dengan masalah kualitas yang dirancang."""
    rng = np.random.default_rng(SEED)
    tanggal = pd.date_range(MULAI, AKHIR, freq="D")
    potongan = []
    for outlet in OUTLET:
        posisi = np.arange(JUMLAH_HARI, dtype="float64")
        tren = 1.0 + TREN_UNIT[outlet] * (posisi / (JUMLAH_HARI - 1))
        musim = 1.0 + np.array(BONUS_DOW)[tanggal.dayofweek]
        unit = UNIT_DASAR[outlet] * tren * musim * rng.uniform(0.88, 1.12, JUMLAH_HARI)
        unit = np.round(unit).astype("int64")
        omzet = unit * HARGA_RATA[outlet] * rng.uniform(0.95, 1.05, JUMLAH_HARI)
        omzet = (np.round(omzet / 100.0) * 100).astype("int64")
        potongan.append(pd.DataFrame({
            "outlet": outlet,
            "tanggal": tanggal,
            "unit_terjual": unit,
            "omzet": omzet,
        }))
    df = pd.concat(potongan, ignore_index=True)

    # semua masalah ditanam di tanggal interior (bukan 1 Apr / 30 Sep)
    # agar interpolasi bawaan tetap bisa mengisi tepi deret waktu
    terpakai = set()
    omzet_salah = pilih_koordinat(rng, 6, terpakai); terpakai.update(omzet_salah)
    omzet_kosong = pilih_koordinat(rng, 5, terpakai); terpakai.update(omzet_kosong)
    unit_kosong = pilih_koordinat(rng, 3, terpakai); terpakai.update(unit_kosong)
    tanggal_hilang = pilih_hilang(rng, terpakai)
    terpakai.update(tanggal_hilang)
    baris_ganda = pilih_koordinat(rng, JUMLAH_DUPLIKAT, terpakai)

    indeks = lambda koordinat: [o * JUMLAH_HARI + h for o, h in koordinat]

    # 1) omzet 0 (3 baris) dan negatif tanda terbalik (3 baris)
    iks = indeks(omzet_salah)
    df.loc[iks[:3], "omzet"] = 0
    df.loc[iks[3:], "omzet"] = -df.loc[iks[3:], "omzet"]

    # 2) omzet kosong (5 baris) dan unit_terjual kosong (3 baris)
    df.loc[indeks(omzet_kosong), "omzet"] = np.nan
    df.loc[indeks(unit_kosong), "unit_terjual"] = np.nan

    # 3) tanggal hilang (baris utuh dibuang)
    df = df.drop(index=indeks(tanggal_hilang)).reset_index(drop=True)

    # 4) baris ganda (8 salinan persis), lalu urutan baris diacak
    df = pd.concat([df, df.loc[indeks(baris_ganda)]], 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_mentah()
    with tempfile.TemporaryDirectory() as folder:
        berkas = Path(folder) / "penjualan_harian.csv"
        df.to_csv(berkas, index=False)
        print(
            f"penjualan_harian.csv siap: {df.shape[0]} baris x {df.shape[1]} kolom "
            f"({berkas.stat().st_size} byte)"
        )

Output yang Dihasilkan:

text
penjualan_harian.csv siap: 546 baris x 4 kolom (19797 byte)

Fungsi buat_mentah() mengembalikan 546 baris: 538 baris outlet-hari asli ditambah 8 salinan persis. Blok if __name__ == "__main__" menuliskan dataset ke penjualan_harian.csv di dalam tempfile.TemporaryDirectory() untuk melaporkan ukurannya; folder sementara itu terhapus otomatis. Pada pekerjaan nyata, argumen read_csv() cukup menunjuk berkas ekspor kasir Anda.

Memuat CSV dan Menjadikan DatetimeIndex #

Skrip berikut menjalankan enam langkah persiapan sekaligus, dari memuat CSV sampai melengkapi kalender tiap outlet.

python
"""Menyiapkan deret waktu penjualan harian Toko Roti Manis.

Memuat CSV mentah, mengubah kolom tanggal menjadi DatetimeIndex,
mendeteksi tanggal ganda dan tanggal hilang, memvalidasi omzet, lalu
melengkapi kalender tiap outlet dan mengisi nilai yang hilang.
"""
import tempfile
from pathlib import Path

import pandas as pd

from buat_dataset import AKHIR, MULAI, OUTLET, buat_mentah

PENUH = pd.date_range(MULAI, AKHIR, freq="D")

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

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

    print("\n== 2. Konversi tanggal dan sort index ==")
    df["tanggal"] = pd.to_datetime(df["tanggal"])
    print(f"dtype tanggal : {df['tanggal'].dtype} | NaT: {df['tanggal'].isna().sum()}")
    df = df.set_index("tanggal").sort_index()
    print(f"index terurut : {df.index.is_monotonic_increasing}")

    print("\n== 3. Tanggal ganda ==")
    print(f"baris duplikat persis   : {df.duplicated().sum()}")
    df = df.drop_duplicates()
    print(f"setelah drop_duplicates : {len(df)} baris")
    sisa_ganda = df.reset_index().duplicated(subset=["outlet", "tanggal"]).sum()
    print(f"pasangan (outlet, tanggal) ganda tersisa: {sisa_ganda}")

    print("\n== 4. Tanggal hilang per outlet ==")
    total_hilang = 0
    for outlet in OUTLET:
        hilang = PENUH.difference(df.loc[df["outlet"] == outlet].index)
        total_hilang += len(hilang)
        print(f"{outlet:11s}: {len(hilang)} tanggal hilang")
    print(f"total: {total_hilang} dari {len(OUTLET) * len(PENUH)} outlet-hari")

    print("\n== 5. Nilai tidak valid ==")
    print(f"omzet 0 atau negatif : {(df['omzet'] <= 0).sum()}")
    print(f"omzet NaN            : {df['omzet'].isna().sum()}")
    print(f"unit_terjual NaN     : {df['unit_terjual'].isna().sum()}")

    print("\n== 6. Lengkapi kalender dan isi nilai ==")
    df["omzet"] = df["omzet"].mask(df["omzet"] <= 0)
    potongan = []
    for outlet in OUTLET:
        g = df.loc[df["outlet"] == outlet, ["unit_terjual", "omzet"]].reindex(PENUH)
        g["outlet"] = outlet
        g[["unit_terjual", "omzet"]] = g[["unit_terjual", "omzet"]].interpolate(method="time")
        potongan.append(g)
    bersih = pd.concat(potongan).rename_axis("tanggal").sort_index()
    bersih = bersih.round().astype({"unit_terjual": "int64", "omzet": "int64"})
    print(f"bentuk akhir  : {bersih.shape[0]} baris x {bersih.shape[1]} kolom")
    print(f"NaN tersisa   : {int(bersih.isna().sum().sum())}")
    print(f"dtype akhir   : unit_terjual {bersih['unit_terjual'].dtype}, "
          f"omzet {bersih['omzet'].dtype}")
    contoh = bersih.loc["2026-05-19"]
    contoh = contoh.loc[contoh["outlet"] == "Kemang"]
    print(f"contoh terisi : Kemang 2026-05-19 -> unit_terjual "
          f"{int(contoh['unit_terjual'].iloc[0])}, omzet {int(contoh['omzet'].iloc[0])}")

Output yang Dihasilkan:

text
== 1. Memuat CSV mentah ==
bentuk: 546 baris x 4 kolom
outlet              str
tanggal             str
unit_terjual    float64
omzet           float64

== 2. Konversi tanggal dan sort index ==
dtype tanggal : datetime64[us] | NaT: 0
index terurut : True

== 3. Tanggal ganda ==
baris duplikat persis   : 8
setelah drop_duplicates : 538 baris
pasangan (outlet, tanggal) ganda tersisa: 0

== 4. Tanggal hilang per outlet ==
Cihampelas : 4 tanggal hilang
Kemang     : 3 tanggal hilang
Malioboro  : 4 tanggal hilang
total: 11 dari 549 outlet-hari

== 5. Nilai tidak valid ==
omzet 0 atau negatif : 6
omzet NaN            : 5
unit_terjual NaN     : 3

== 6. Lengkapi kalender dan isi nilai ==
bentuk akhir  : 549 baris x 3 kolom
NaN tersisa   : 0
dtype akhir   : unit_terjual int64, omzet int64
contoh terisi : Kemang 2026-05-19 -> unit_terjual 432, omzet 4649050

Mari kita telaah langkah demi langkah:

  1. read_csv() menghasilkan 546 baris x 4 kolom: kolom teks terbaca str (perilaku bawaan pandas 3.0), sementara unit_terjual dan omzet langsung menjadi float64 karena mengandung NaN.
  2. to_datetime() lalu set_index("tanggal").sort_index(): kolom tanggal berubah menjadi datetime64[us] (resolusi mikrodetik bawaan pandas 3.0) dengan 0 NaT, kemudian menjadi index terurut naik.
  3. duplicated() dan drop_duplicates(): delapan salinan persis dibuang sehingga tersisa 538 baris; pemeriksaan duplicated(subset=["outlet", "tanggal"]) memastikan tidak ada pasangan outlet-tanggal ganda yang tersisa.
  4. PENUH.difference(index_per_outlet): kalender lengkap 183 hari dibuat dengan pd.date_range(); selisihnya terhadap tanggal tercatat per outlet menemukan 11 tanggal hilang (4 Cihampelas, 3 Kemang, 4 Malioboro).
  5. mask(df["omzet"] <= 0): enam omzet 0 atau negatif diubah menjadi NaN agar diperlakukan sebagai nilai hilang, bukan angka sah.
  6. reindex(PENUH) dan interpolate(method="time") per outlet: tiap deret outlet dilengkapi menjadi 183 baris (22 NaN omzet, 14 NaN unit), diisi interpolasi berbasis jarak waktu, lalu dikembalikan ke int64 — baris Kemang 19 Mei yang hilang terisi 432 unit dan omzet 4.649.050.

Lima Masalah dan Penanganannya #

Rekap di atas merangkum kelima masalah dalam angka pasti:

MasalahJumlahPenanganan
Baris duplikat persis8drop_duplicates()
Tanggal hilang per outlet11reindex() kalender 183 hari
Omzet 0 atau negatif6mask() menjadi NaN
Omzet NaN5interpolate(method="time")
unit_terjual NaN3interpolate(method="time")

Deret bersih 549 baris ini belum banyak bercerita: kita masih melihat satu baris per outlet per hari. Bagian 2 merangkumnya ke periode mingguan dan bulanan dengan resample(), sehingga arah penjualan tiap outlet mulai terlihat.

Sumber bacaan resmi: pandas.to_datetime , pandas.DataFrame.set_index , dan pandas.Series.interpolate .