Bagian 1 menutup dengan temuan yang tegas: autocorrelation lag 7 sebesar 0,761 menandakan nilai sepekan lalu sebagai prediktor tunggal terkuat deret penjualan. Sebelum model machine learning disentuh, temuan tersebut harus diberi bentuk paling sederhana — baseline statistik. Aturan mainnya sudah lama menjadi konsensus praktik: model yang tidak mampu mengalahkan aturan naive satu baris kode tidak layak dipakai di produksi, sebab kompleksitasnya tidak menukar apa pun. Pada bagian ini kita menilai tiga baseline — naive nilai terakhir, rata-rata 7 hari, dan seasonal-naive mingguan — pada horizon 14 hari terakhir deret dengan split kronologis yang ketat, lalu menetapkan pembanding resmi yang harus dikalahkan model Bagian 3.

Tiga Baseline dan Disiplin Split Kronologis #

Evaluasi forecasting memakai holdout di ujung deret: seluruh 716 hari pertama menjadi history, dan 14 hari terakhir — 2024-12-17 sampai 2024-12-30 — menjadi data uji yang tidak boleh dibaca model sebelum meramal. Split ini harus dilakukan dengan mengiris deret menurut urutan waktu, bukan dengan pengacakan baris. Alasannya mekanis dan tidak bisa ditawar: pada deret waktu, baris masa depan memuat informasi tentang baris masa lalu; pengacakan membuat informasi bocor ke data latih dan skor validasi menjadi terlalu optimis.

Ketiga baseline bekerja tanpa pembelajaran apa pun. Naive memakai nilai terakhir history (146 unit) sebagai ramalan tetap selama 14 hari. Rata-rata 7 hari memakai konstanta lain — rata-rata pekan terakhir history sebesar 164,4 unit. Seasonal-naive mingguan mengulang blok tujuh hari terakhir secara siklis: Sabtu lalu meramal Sabat ini, Rabu lalu meramal Rabu ini, dan seterusnya, sehingga pola akhir pekan ikut terbawa ke depan.

python
# Baseline statistik untuk forecasting penjualan 14 hari ke depan.
# Tiga pembanding sederhana dinilai pada 14 hari terakhir deret
# (holdout kronologis, BUKAN split acak):
#   1. naive             : ramalkan besok = nilai hari ini (diulang terus).
#   2. rata-rata 7 hari  : konstanta = rata-rata 7 hari terakhir latih.
#   3. seasonal-naive    : ulang blok 7 hari terakhir latih secara siklis.
# Metrik: MAE dan RMSE pada horizon 14 hari.
# Semua angka dihitung dari data/penjualan.csv hasil src/01_bangun_data.py.
# Jalankan dari root proyek: python3 src/03_baseline.py

import numpy as np
import pandas as pd

HORIZON = 14

df = pd.read_csv("data/penjualan.csv", parse_dates=["tanggal"])
y = df["penjualan_unit"].to_numpy()
tanggal = df["tanggal"].dt.strftime("%Y-%m-%d").to_list()

# ---------- Split kronologis ----------
# Data latih = seluruh history sebelum horizon; data uji = 14 hari terakhir.
# Urutan waktu dijaga penuh: tidak ada pengacakan baris.
latih = y[:-HORIZON]
uji = y[-HORIZON:]
tgl_uji = tanggal[-HORIZON:]
print("== Split kronologis (tanpa pengacakan) ==")
print(f"data latih : {len(latih)} hari ({tanggal[0]} s/d {tanggal[-HORIZON - 1]})")
print(f"data uji   : {HORIZON} hari ({tgl_uji[0]} s/d {tgl_uji[-1]})")
print(f"rata-rata latih {latih.mean():.1f} | rata-rata uji {uji.mean():.1f}")

# ---------- Tiga baseline ----------
pred_naive = np.repeat(latih[-1], HORIZON)
pred_ma7 = np.repeat(latih[-7:].mean(), HORIZON)
pred_seasonal = np.tile(latih[-7:], HORIZON // 7)

def mae(aktual, prediksi):
    return float(np.mean(np.abs(aktual - prediksi)))

def rmse(aktual, prediksi):
    return float(np.sqrt(np.mean((aktual - prediksi) ** 2)))

hasil = [
    ("naive (nilai terakhir)", pred_naive),
    ("rata-rata 7 hari", pred_ma7),
    ("seasonal-naive mingguan", pred_seasonal),
]

# ---------- Prediksi harian keempat kolom ----------
print("\n== Prediksi harian 3 baseline vs aktual (14 hari uji) ==")
print(f"{'tanggal':12s} {'aktual':>7s} {'naive':>7s} {'ma7':>7s} {'seas':>7s}")
for i in range(HORIZON):
    print(
        f"{tgl_uji[i]:12s} {uji[i]:7d} {pred_naive[i]:7.1f}"
        f" {pred_ma7[i]:7.1f} {pred_seasonal[i]:7.1f}"
    )

# ---------- Tabel skor ----------
print("\n== Skor baseline pada horizon 14 hari ==")
print(f"{'model':26s} {'MAE':>7s} {'RMSE':>7s}")
for nama, pred in hasil:
    print(f"{nama:26s} {mae(uji, pred):7.2f} {rmse(uji, pred):7.2f}")

terbaik = min(hasil, key=lambda p: mae(uji, p[1]))
print(f"\nbaseline terbaik menurut MAE : {terbaik[0]}")
print(f"MAE baseline terbaik         : {mae(uji, terbaik[1]):.2f} unit/hari")
print(
    "ingat: 14 hari uji memuat hari libur 2024-12-25 dan promo 2024-12-27;\n"
    "ketiganya tidak punya informasi kalender, jadi event tersebut terlewat."
)

Output yang Dihasilkan:

text
== Split kronologis (tanpa pengacakan) ==
data latih : 716 hari (2023-01-01 s/d 2024-12-16)
data uji   : 14 hari (2024-12-17 s/d 2024-12-30)
rata-rata latih 139.1 | rata-rata uji 165.4

== Prediksi harian 3 baseline vs aktual (14 hari uji) ==
tanggal       aktual   naive     ma7    seas
2024-12-17       139   146.0   164.4   158.0
2024-12-18       156   146.0   164.4   149.0
2024-12-19       152   146.0   164.4   161.0
2024-12-20       158   146.0   164.4   157.0
2024-12-21       188   146.0   164.4   189.0
2024-12-22       203   146.0   164.4   191.0
2024-12-23       136   146.0   164.4   146.0
2024-12-24       140   146.0   164.4   158.0
2024-12-25       100   146.0   164.4   149.0
2024-12-26       168   146.0   164.4   161.0
2024-12-27       250   146.0   164.4   157.0
2024-12-28       196   146.0   164.4   189.0
2024-12-29       183   146.0   164.4   191.0
2024-12-30       147   146.0   164.4   146.0

== Skor baseline pada horizon 14 hari ==
model                          MAE    RMSE
naive (nilai terakhir)       29.29   40.32
rata-rata 7 hari             27.78   35.35
seasonal-naive mingguan      17.29   29.61

baseline terbaik menurut MAE : seasonal-naive mingguan
MAE baseline terbaik         : 17.29 unit/hari
ingat: 14 hari uji memuat hari libur 2024-12-25 dan promo 2024-12-27;
ketiganya tidak punya informasi kalender, jadi event tersebut terlewat.

Mari kita bedah skrip baseline baris-per-baris:

  1. Baris 14 (HORIZON = 14): horizon evaluasi dibekukan dua pekan — periode yang cukup panjang untuk menghukum ramalan datar, cukup pendek untuk tetap realistis bagi perencanaan stok mingguan.
  2. Baris 16–18 (muat deret): CSV dibaca dengan parse_dates, lalu nilai penjualan dan daftar tanggal dipisah menjadi array dan list — slicing kronologis tinggal operasi indeks negatif.
  3. Baris 23–25 (split kronologis): y[:-14] dan y[-14:] mengiris tanpa mengacak; 716 hari history dan 14 hari uji tidak pernah bertukar posisi.
  4. Baris 32–34 (tiga ramalan): np.repeat menggandakan satu nilai menjadi 14, np.tile menggandakan blok 7 hari menjadi dua pekan — dua fungsi NumPy yang memuat seluruh logika baseline.
  5. Baris 36–40 (metrik): MAE menghitung rata-rata selisih absolut; RMSE menghitung akar rata-rata kuadrat selisih sehingga galat besar dihukum lebih berat daripada galat kecil.
  6. Baris 49–55 (tabel harian): keempat kolom — aktual dan tiga ramalan — dicetak berdampingan per tanggal agar kegagalan tiap baseline bisa dilacak ke hari tertentu.
  7. Baris 58–61 (tabel skor): MAE dan RMSE ketiga baseline dirangkum dalam satu tabel perbandingan.
  8. Baris 63–68 (pemilihan pembanding): min atas daftar kandidat menurut MAE menetapkan baseline terbaik; catatan penutup mengingatkan bahwa dua event kalender di jendela uji tidak mungkin tertangkap ketiga baseline.

Membaca Hasil: Musiman Mingguan Menyelamatkan Prediksi #

BaselineMAE (unit/hari)RMSE (unit/hari)
Naive (nilai terakhir)29,2940,32
Rata-rata 7 hari27,7835,35
Seasonal-naive mingguan17,2929,61

Tabel di atas memperlihatkan jurang yang lebar. Naive mengunci ramalan pada 146 unit — nilai hari Senin terakhir history — sehingga seluruh lonjakan akhir pekan terlewat: hari Sabtu dan Minggu di jendela uji mencapai 188–203 unit sementara ramalan tetap 146. Rata-rata 7 hari sedikit membaik karena konstanta 164,4 kebetulan dekat level mingguan jendela uji, namun ramalan datar tetap tidak bisa mengikuti naik-turun hari ke hari. Seasonal-naive mingguan memangkas MAE hampir separuh dari naive menjadi 17,29 karena pola mingguan ikut terulang — bukti langsung bahwa musiman mingguan adalah struktur dominan deret ini, persis seperti yang dijanjikan autocorrelation lag 7.

Dua angka lain layak dibaca cermat. Pertama, RMSE seasonal-naive (29,61) jauh di atas MAE-nya (17,29) — tanda bahwa sejumlah kecil hari bergalat sangat besar. Dua hari itulah sumbernya: hari libur 2024-12-25 (aktual 100 unit, ramalan 149) dan promo 2024-12-27 (aktual 250 unit, ramalan 157). Kedua, rata-rata data uji 165,4 unit sementara rata-rata history hanya 139,1 unit — jendela uji berada pada level tertinggi deret karena tren dan puncak musiman Desember bertemu, sehingga ramalan yang belajar level rata-rata history otomatis tertinggal.

Tiga baseline kini dinilai dan pembanding resmi ditetapkan: seasonal-naive mingguan dengan MAE 17,29 unit per hari. Pertanyaan Bagian 3 mengalir dari angka tersebut — dapatkah model machine learning yang belajar dari fitur lag dan kalender mingguan mengalahkan aturan sederhana “ulang pekan lalu”, dan dengan margin seberapa besar? Jawabannya dikerjakan lewat rekayasa fitur pada Bagian 3: Model Machine Learning.

Referensi Resmi #