Setiap proyek forecasting dimulai dari pertanyaan yang sama: pola apa yang tertanam di balik angka harian, dan seberapa jauh pola tersebut bisa dipercaya untuk meramal? Berbeda dengan tabel pada proyek sebelumnya, data penjualan harian mengemban dimensi yang tidak boleh dilanggar: urutan waktu — menukar dua baris mengubah makna deret total. Kita akan bangkitkan dataset sintetis 730 hari, memeriksa kualitas dan kesinambungannya, lalu membaca tiga pola yang menentukan desain baseline dan model: musiman mingguan, tren naik ringan, dan lonjakan event kalender.

Membangkitkan Deret Penjualan Dua Tahun #

Proyek ini memakai data sintetis agar hasilnya reproducible bit demi bit: satu generator np.random.default_rng(42) menentukan seluruh derau, dan tanggal dimulai dari literal 2023-01-01 — bukan tanggal komputer — sehingga CSV tidak berubah kapan pun skrip dijalankan ulang. Pembangkit menjumlahkan enam komponen lazim ritel: level dasar 120 unit per hari, tren naik 0,05 unit per hari, musiman mingguan berakhir pekan tinggi, musiman tahunan ringan berpuncak Desember, lonjakan promo serta penurunan hari libur pada tanggal tetap, dan derau normal.

Desain ini meniru dilema nyata perencanaan stok: komponen mingguan dan tahunan bisa dipelajari model, sedangkan event kalender hanya bisa dimanfaatkan bila tanggalnya diketahui di muka. Dua belas tanggal event sengaja ditanam — delapan promo dan empat hari libur — dan skrip mencetak nilainya sebagai bukti keberadaan lonjakan serta penurunan di data.

python
# Bangkitkan deret penjualan harian toko ritel sintetis untuk proyek
# forecasting. Deterministik penuh: satu generator np.random.default_rng(42)
# dan tanggal literal mulai 2023-01-01 (bukan tanggal komputer yang berjalan),
# sehingga siapa pun yang menjalankan skrip mendapat CSV identik.
# Jalankan dari root proyek: python3 src/01_bangun_data.py
# Keluaran: data/penjualan.csv (730 baris, kolom tanggal + penjualan_unit).
#
# Komponen deret yang ditanam:
#   1. level dasar      : 120 unit/hari.
#   2. tren naik ringan : +0,05 unit/hari (sekitar +30% dalam dua tahun).
#   3. musiman mingguan : Senin-Kamis di bawah rata-rata, Jumat dekat
#      rata-rata, Sabtu-Minggu tinggi (pola akhir pekan).
#   4. musiman tahunan  : puncak pertengahan Desember, lembah pertengahan
#      Juni, amplitudo 9 unit.
#   5. event kalender   : lonjakan promo dan penurunan hari libur pada
#      tanggal tetap (kamus PROMO dan LIBUR di bawah).
#   6. derau            : normal(0, 9).

import os

import numpy as np
import pandas as pd

SEED = 42
N_HARI = 730
TANGGAL_MULAI = "2023-01-01"
NAMA_BERKAS = "data/penjualan.csv"

# Offset musiman mingguan per hari-dalam-minggu, Senin..Minggu (0..6).
OFFSET_MINGGUAN = np.array([-8.0, -12.0, -15.0, -10.0, 2.0, 24.0, 28.0])

# Lonjakan promo (unit ekstra) pada tanggal tetap selama dua tahun.
PROMO = {
    "2023-05-05": 55.0,   # promo tanggal kembar 5.5
    "2023-08-17": 40.0,   # promo hari kemerdekaan
    "2023-11-11": 90.0,   # promo 11.11
    "2023-12-22": 75.0,   # promo akhir tahun
    "2024-05-05": 60.0,
    "2024-08-17": 45.0,
    "2024-11-11": 100.0,
    "2024-12-27": 80.0,
}

# Penurunan hari libur (unit berkurang): toko buka lebih singkat.
LIBUR = {
    "2023-01-01": -45.0,
    "2023-12-25": -40.0,
    "2024-01-01": -50.0,
    "2024-12-25": -42.0,
}

rng = np.random.default_rng(SEED)

# ---------- Susun komponen deret ----------
tanggal = pd.date_range(TANGGAL_MULAI, periods=N_HARI, freq="D")
hari_ke = np.arange(N_HARI)

tren = 120.0 + 0.05 * hari_ke
musiman_mingguan = OFFSET_MINGGUAN[tanggal.dayofweek.to_numpy()]

# Musiman tahunan sebagai kosinus hari-ke-tahun: bernilai penuh +9 saat
# hari-ke-tahun 350 (pertengahan Desember) dan -9 saat 165 (pertengahan Juni).
hari_ke_tahun = tanggal.dayofyear.to_numpy()
musiman_tahunan = 9.0 * np.cos(2.0 * np.pi * (hari_ke_tahun - 350.0) / 365.25)

# Event kalender ditempatkan lewat peta tanggal -> indeks baris.
peta_index = {ts.strftime("%Y-%m-%d"): i for i, ts in enumerate(tanggal)}
event = np.zeros(N_HARI)
for tgl, tambahan in PROMO.items():
    event[peta_index[tgl]] += tambahan
for tgl, pengurang in LIBUR.items():
    event[peta_index[tgl]] += pengurang

derau = rng.normal(0.0, 9.0, size=N_HARI)

penjualan = tren + musiman_mingguan + musiman_tahunan + event + derau
penjualan = np.clip(np.round(penjualan), 5, None).astype(int)

# ---------- Tulis CSV ----------
df = pd.DataFrame(
    {
        "tanggal": tanggal.strftime("%Y-%m-%d"),
        "penjualan_unit": penjualan,
    }
)
os.makedirs("data", exist_ok=True)
df.to_csv(NAMA_BERKAS, index=False)

# ---------- Ringkasan untuk pemeriksaan ----------
print("== Ringkasan pembangkitan data ==")
print(f"jumlah baris : {len(df)}")
print(f"kolom        : {list(df.columns)}")
print(f"rentang      : {df['tanggal'].iloc[0]} s/d {df['tanggal'].iloc[-1]}")
print(f"berkas       : {NAMA_BERKAS}")
print(f"rata-rata    : {df['penjualan_unit'].mean():.1f} unit/hari")
print(f"minimum      : {df['penjualan_unit'].min()} unit")
print(f"maksimum     : {df['penjualan_unit'].max()} unit")

print("\n== Nilai pada tanggal event kalender (bukti lonjakan/penurunan) ==")
print(f"{'tanggal':12s} {'event':>7s} {'penjualan':>10s}")
for tgl in sorted(set(PROMO) | set(LIBUR)):
    nilai = int(df.loc[df["tanggal"] == tgl, "penjualan_unit"].iloc[0])
    if tgl in PROMO:
        print(f"{tgl:12s} {'promo':>7s} {nilai:10d}")
    else:
        print(f"{tgl:12s} {'libur':>7s} {nilai:10d}")

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

Output yang Dihasilkan:

text
== Ringkasan pembangkitan data ==
jumlah baris : 730
kolom        : ['tanggal', 'penjualan_unit']
rentang      : 2023-01-01 s/d 2024-12-30
berkas       : data/penjualan.csv
rata-rata    : 139.6 unit/hari
minimum      : 88 unit
maksimum     : 265 unit

== Nilai pada tanggal event kalender (bukti lonjakan/penurunan) ==
tanggal        event  penjualan
2023-01-01     libur        114
2023-05-05     promo        184
2023-08-17     promo        149
2023-11-11     promo        265
2023-12-22     promo        226
2023-12-25     libur         91
2024-01-01     libur        102
2024-05-05     promo        235
2024-08-17     promo        203
2024-11-11     promo        261
2024-12-25     libur        100
2024-12-27     promo        250

== Lima baris pertama ==
   tanggal  penjualan_unit
2023-01-01             114
2023-01-02             111
2023-01-03             123
2023-01-04             122
2023-01-05             101

Mari kita bedah skrip pembangkit data baris-per-baris:

  1. Baris 24–27 (konstanta): SEED = 42, 730 hari, tanggal mulai literal, dan nama berkas — tanpa jam sistem, CSV identik di mesin mana pun.
  2. Baris 29–30 (OFFSET_MINGGUAN): tujuh offset Senin sampai Minggu; Rabu terendah (−15), Minggu tertinggi (+28) — rentang hampir 43 unit.
  3. Baris 32–50 (PROMO dan LIBUR): dua belas tanggal event berdampak dalam unit; promo 11.11 diberi dampak terbesar (90 dan 100 unit).
  4. Baris 55–64 (tren dan musiman): tren berupa garis lurus 120 + 0,05 × hari; offset mingguan dipilih lewat indeks dayofweek; kosinus hari-ke-tahun: +9 di pertengahan Desember, −9 di pertengahan Juni.
  5. Baris 66–72 (penempatan event): peta tanggal-ke-indeks menaruh promo dan libur pada baris yang tepat sebagai komponen event.
  6. Baris 74–77 (derau dan penjumlahan): derau normal(0, 9) ditarik dari generator berseed; keenam komponen dijumlahkan, dibulatkan, dipangkas minimum 5 unit.
  7. Baris 80–87 (penulisan CSV): dua kolom — tanggal format YYYY-MM-DD dan penjualan_unit bilangan bulat.
  8. Baris 99–106 (tabel event): nilai aktual ke-12 tanggal event dicetak sebagai pemeriksaan desain.

Memeriksa Kualitas Deret dan Membaca Pola #

Deret waktu punya gerbang kualitas tambahan yang tidak ada pada tabel biasa: kesinambungan. Satu hari yang hilang dari deret harian akan menggeser seluruh perhitungan lag tanpa memicu galat apa pun, sehingga selisih antar-tanggal wajib diperiksa secara eksplisit. Setelah gerbang itu terlewati, eksplorasi membaca pola lewat tiga kaca mata: rata-rata per hari-dalam-minggu, rata-rata bulanan, serta autocorrelation yang mengukur seberapa kuat nilai hari ini diprediksi nilai hari-hari sebelumnya.

Karena contoh ini berjalan tanpa tampilan grafik, tabel bulanan dilengkapi batang teks — satu karakter # mewakili sekitar lima unit — sebagai pengganti plot garis. Pola yang sama tetap terbaca: batang memanjang ke akhir tahun, menyusut saat berganti tahun, lalu memanjang lagi.

python
# Eksplorasi deret waktu penjualan: muat data/penjualan.csv, periksa
# kualitas dan kesinambungan harian, lalu baca pola lewat angka:
# rata-rata per hari-dalam-minggu, tren bulanan (beserta batang teks
# sebagai pengganti plot), autocorrelation lag 1/7/14/30, dan hari-hari
# ekstrem. Semua angka dihitung dari berkas CSV hasil src/01_bangun_data.py.
# Jalankan dari root proyek: python3 src/02_eksplorasi.py

import numpy as np
import pandas as pd

df = pd.read_csv("data/penjualan.csv", parse_dates=["tanggal"])
y = df["penjualan_unit"].to_numpy()

# ---------- Kualitas data ----------
print("== Muat data ==")
print(f"bentuk tabel : {len(df)} baris x {df.shape[1]} kolom")
print(f"nilai hilang : {int(df.isna().sum().sum())}")
print(f"tanggal ganda: {int(df['tanggal'].duplicated().sum())}")
selisih = df["tanggal"].diff().dropna().dt.days.unique()
print(f"rentang      : {df['tanggal'].iloc[0].date()} s/d {df['tanggal'].iloc[-1].date()}")
print(f"jarak hari   : {'semua 1 hari (deret harian murni)' if set(selisih) == {1} else 'TIDAK seragam'}")
print(f"rata-rata    : {y.mean():.1f} unit/hari  (std {y.std():.1f})")

# ---------- Pola mingguan ----------
HARI = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"]
print("\n== Rata-rata penjualan per hari-dalam-minggu ==")
print(f"{'hari':8s} {'rata-rata':>10s} {'selisih vs umum':>16s}")
for d in range(7):
    rata = df.loc[df["tanggal"].dt.dayofweek == d, "penjualan_unit"].mean()
    print(f"{HARI[d]:8s} {rata:10.1f} {rata - y.mean():+16.1f}")

# ---------- Tren bulanan ----------
print("\n== Rata-rata bulanan (batang = pengganti plot, 1 '#' ~ 5 unit) ==")
bulan = df["tanggal"].dt.to_period("M").astype(str)
rata_bulanan = df.groupby(bulan)["penjualan_unit"].mean()
for nama_bulan, nilai in rata_bulanan.items():
    batang = "#" * int(round(nilai / 5))
    print(f"{nama_bulan} {batang} {nilai:6.1f}")

# ---------- Bukti tren naik ----------
awal = y[:30].mean()
akhir = y[-30:].mean()
print(f"\nrata-rata 30 hari pertama : {awal:6.1f}")
print(f"rata-rata 30 hari terakhir: {akhir:6.1f}")
print(f"kenaikan                 : {akhir - awal:+6.1f} unit "
      f"({(akhir - awal) / awal:+.1%})")

# ---------- Autocorrelation ----------
def autocorrelation(x, lag):
    """Korelasi Pearson antara deret dan salinannya yang digeser `lag` hari."""
    return float(np.corrcoef(x[:-lag], x[lag:])[0, 1])

print("\n== Autocorrelation deret penjualan ==")
for lag in [1, 7, 14, 30]:
    print(f"lag {lag:2d} hari : {autocorrelation(y, lag):+.3f}")

# ---------- Hari ekstrem ----------
print("\n== Lima hari penjualan tertinggi ==")
atas = df.nlargest(5, "penjualan_unit")[["tanggal", "penjualan_unit"]]
for _, baris in atas.iterrows():
    print(f"{baris['tanggal'].date()}  {int(baris['penjualan_unit']):4d} unit")
print("\n== Tiga hari penjualan terendah ==")
bawah = df.nsmallest(3, "penjualan_unit")[["tanggal", "penjualan_unit"]]
for _, baris in bawah.iterrows():
    print(f"{baris['tanggal'].date()}  {int(baris['penjualan_unit']):4d} unit")

Output yang Dihasilkan:

text
== Muat data ==
bentuk tabel : 730 baris x 2 kolom
nilai hilang : 0
tanggal ganda: 0
rentang      : 2023-01-01 s/d 2024-12-30
jarak hari   : semua 1 hari (deret harian murni)
rata-rata    : 139.6 unit/hari  (std 24.4)

== Rata-rata penjualan per hari-dalam-minggu ==
hari      rata-rata  selisih vs umum
Senin         130.3             -9.3
Selasa        125.3            -14.3
Rabu          123.1            -16.5
Kamis         127.6            -12.0
Jumat         141.9             +2.3
Sabtu         162.1            +22.4
Minggu        166.9            +27.2

== Rata-rata bulanan (batang = pengganti plot, 1 '#' ~ 5 unit) ==
2023-01 ##########################  129.2
2023-02 ##########################  128.1
2023-03 #########################  123.6
2023-04 ########################  121.4
2023-05 ########################  120.9
2023-06 ########################  120.0
2023-07 #########################  124.6
2023-08 #########################  126.8
2023-09 ###########################  135.6
2023-10 ############################  140.4
2023-11 ##############################  149.4
2023-12 ##############################  150.5
2024-01 #############################  145.2
2024-02 #############################  144.2
2024-03 #############################  146.1
2024-04 ############################  139.9
2024-05 ############################  138.3
2024-06 ############################  140.0
2024-07 ############################  140.4
2024-08 #############################  146.6
2024-09 ##############################  151.8
2024-10 ###############################  155.6
2024-11 #################################  167.0
2024-12 #################################  165.5

rata-rata 30 hari pertama :  129.0
rata-rata 30 hari terakhir:  165.5
kenaikan                 :  +36.6 unit (+28.4%)

== Autocorrelation deret penjualan ==
lag  1 hari : +0.529
lag  7 hari : +0.761
lag 14 hari : +0.737
lag 30 hari : +0.122

== Lima hari penjualan tertinggi ==
2023-11-11   265 unit
2024-11-11   261 unit
2024-12-27   250 unit
2024-05-05   235 unit
2023-12-22   226 unit

== Tiga hari penjualan terendah ==
2023-05-23    88 unit
2023-12-25    91 unit
2023-04-05    92 unit

Bedah kode skrip eksplorasi:

  1. Baris 11–12 (pd.read_csv): kolom tanggal diurai sebagai datetime agar operasi kalender (dayofweek, to_period) tersedia; seluruh angka dihitung dari CSV, bukan memori skrip pembangkit.
  2. Baris 17–21 (gerbang kualitas deret): nilai hilang dan tanggal ganda diperiksa, lalu diff().dt.days memastikan jarak antar-baris selalu satu hari — deret harian murni tanpa lubang.
  3. Baris 28–30 (rata-rata per hari-minggu): loop tujuh hari menghitung rata-rata dan selisihnya terhadap rata-rata umum 139,6 unit.
  4. Baris 34–38 (tren bulanan): to_period("M") mengelompokkan baris per bulan kalender; batang teks diskalakan dari nilai bulanan sehingga panjangnya proporsional terhadap level penjualan.
  5. Baris 41–46 (bukti tren): rata-rata 30 hari pertama lawan 30 hari terakhir — 129,0 lawan 165,5 unit, kenaikan 28,4%.
  6. Baris 49–51 (fungsi autocorrelation): korelasi Pearson antara deret dan salinannya yang digeser lag hari, dihitung dengan np.corrcoef.
  7. Baris 53–55 (lag 1/7/14/30): empat lag dicetak berjajar; lag 7 (0,761) jauh melampaui lag 1 (0,529) dan lag 30 (0,122).
  8. Baris 59–65 (hari ekstrem): nlargest dan nsmallest memperlihatkan nilai-nilai ekstrem — bukti empiris keberadaan event kalender.

Temuan yang Menuntun Desain Baseline dan Model #

Tabel berikut merangkum musiman mingguan — temuan paling menentukan bagi desain baseline:

HariRata-rata (unit)Selisih vs rata-rata umum
Senin130,3−9,3
Selasa125,3−14,3
Rabu123,1−16,5
Kamis127,6−12,0
Jumat141,9+2,3
Sabtu162,1+22,4
Minggu166,9+27,2

Empat temuan utama keluar dari eksplorasi ini. Pertama, musiman mingguan kuat dan teratur: Sabtu serta Minggu berkedudukan 22–27 unit di atas rata-rata umum, sementara hari kerja turun 9–17 unit. Kedua, tren naik nyata — 28,4% dalam dua tahun. Ketiga, siklus tahunan melengkapi gambar: rata-rata bulanan turun ke 120,0 pada pertengahan 2023 lalu memuncak 167,0 pada November 2024. Keempat, autocorrelation lag 7 sebesar 0,761 menegaskan nilai sepekan lalu sebagai prediktor tunggal terkuat — alasan seasonal-naive mingguan menjadi baseline utama Bagian 2.

Kelima hari tertinggi seluruhnya tanggal promo — 265 unit pada 2023-11-11 dan 261 unit pada 2024-11-11 — sementara hari libur bergerak sebaliknya: 2023-12-25 (91 unit) masuk tiga hari terendah, dan 2024-12-25 (100 unit) jauh di bawah hari kerja normal. Event kalender menjadi sumber galat terbesar yang bisa diperkirakan — kita sengaja membiarkannya sebagai batas model tanpa fitur kalender. Karakter deret kini terpetakan; langkah berikutnya menetapkan pembanding minimal yang harus dikalahkan — tiga baseline statistik pada split kronologis — di Bagian 2: Baseline Statistik.

Referensi Resmi #