SARIMA pada chapter sebelumnya menangkap musiman mingguan lewat orde musiman, tetapi jalur statistik bukan satu-satunya cara meramalkan. Pendekatan kedua memanfaatkan keahlian yang sudah kita bangun sejak awal Course ini: supervised learning. Triknya adalah reframing — jendela nilai lampau diubah menjadi kolom fitur, nilai berikutnya menjadi target, dan deret waktu pun berubah menjadi matriks tabular yang bisa dimakan regressor scikit-learn mana pun. Kita akan membangun matriks fitur lag dan fitur kalender untuk penjualan Toko Ceria, melatih RandomForestRegressor, lalu mengadunya secara jujur dengan baseline naive pada jendela uji yang sama dengan chapter ARIMA.

Dari Deret ke Matriks Supervised #

Mekanismenya sederhana: untuk setiap hari t, nilai y_(t-1) sampai y_(t-7) menjadi fitur dan y_t menjadi target. Operasi shift(k) pada pandas menggeser deret sebesar k langkah, sehingga kolom lag_k berisi nilai k hari sebelumnya yang sejajar dengan hari ini. Tujuh baris pertama tidak punya riwayat lengkap dan dibuang lewat dropna() — harga yang kita bayar untuk bentuk supervised ini. Di samping fitur lag, indeks tanggal menyumbang fitur kalender: kolom hari (0 = Senin sampai 6 = Minggu) dan akhir_pekan (1 untuk Sabtu–Minggu), keduanya diturunkan dari tanggal acuan tetap deret.

python
# Mengubah deret waktu menjadi matriks supervised: fitur lag dari nilai
# lampau plus fitur kalender (hari dan penanda akhir pekan).

import numpy as np
import pandas as pd


def buat_deret_penjualan(n_hari=168):
    rng = np.random.default_rng(42)
    idx = pd.date_range(start="2026-01-05", periods=n_hari, freq="D")
    t = np.arange(n_hari)
    tren = 120.0 + 0.45 * t
    hari = np.asarray(idx.dayofweek)
    efek_hari = np.select(
        [hari == 4, hari == 5, hari == 6],
        [10.0, 32.0, 40.0],
        default=0.0,
    )
    promo = ((t >= 70) & (t < 84)).astype(float) * 45.0
    kilat = (t == 105).astype(float) * 85.0
    derau = rng.normal(0.0, 12.0, n_hari)
    y = tren + efek_hari + promo + kilat + derau
    return pd.Series(np.round(y, 1), index=idx, name="penjualan")


penjualan = buat_deret_penjualan()

df = pd.DataFrame({"y": penjualan})
for k in range(1, 8):
    df[f"lag_{k}"] = penjualan.shift(k)
df["hari"] = penjualan.index.dayofweek        # 0 = Senin ... 6 = Minggu
df["akhir_pekan"] = (penjualan.index.dayofweek >= 5).astype(int)
df = df.dropna()
fitur = [kolom for kolom in df.columns if kolom != "y"]

print(f"Bentuk matriks supervised: {df.shape[0]} baris x {df.shape[1]} kolom "
      f"(1 target + {len(fitur)} fitur)")

def tampilkan(baris, judul):
    print(f"\n{judul}:")
    kepala = ("  " + f"{'tanggal':10s}  {'y':>6s}  "
              + " ".join(f"{k:>7s}" for k in fitur if k.startswith("lag"))
              + f"   {'hari':>4s}  {'akhir_pekan':>11s}")
    print(kepala)
    for tanggal, baris_isi in baris.iterrows():
        nilai = " ".join(f"{baris_isi[k]:7.1f}" for k in fitur if k.startswith("lag"))
        print(f"  {tanggal.date()}  {baris_isi['y']:6.1f}  {nilai}   {int(baris_isi['hari']):4d}  "
              f"{int(baris_isi['akhir_pekan']):11d}")

tampilkan(df.iloc[:6], "Enam baris pertama")
tampilkan(df.loc["2026-04-17":"2026-04-21"], "Lima baris di sekitar hari diskon kilat")

print("\nKorelasi Pearson tiap fitur dengan target y:")
for kolom in fitur:
    korelasi = np.corrcoef(df[kolom], df["y"])[0, 1]
    print(f"  {kolom:12s} {korelasi:+.3f}")

Output yang Dihasilkan:

text
Bentuk matriks supervised: 161 baris x 10 kolom (1 target + 9 fitur)

Enam baris pertama:
  tanggal          y    lag_1   lag_2   lag_3   lag_4   lag_5   lag_6   lag_7   hari  akhir_pekan
  2026-01-12   119.4    164.2   138.6   108.4   132.6   129.9   108.0   123.7      0            0
  2026-01-13   123.4    119.4   164.2   138.6   108.4   132.6   129.9   108.0      1            0
  2026-01-14   113.8    123.4   119.4   164.2   138.6   108.4   132.6   129.9      2            0
  2026-01-15   135.1    113.8   123.4   119.4   164.2   138.6   108.4   132.6      3            0
  2026-01-16   144.3    135.1   113.8   123.4   119.4   164.2   138.6   108.4      4            0
  2026-01-17   158.2    144.3   135.1   113.8   123.4   119.4   164.2   138.6      5            1

Lima baris di sekitar hari diskon kilat:
  tanggal          y    lag_1   lag_2   lag_3   lag_4   lag_5   lag_6   lag_7   hari  akhir_pekan
  2026-04-17   171.6    181.0   160.5   153.7   168.9   191.7   179.3   155.4      4            0
  2026-04-18   207.2    171.6   181.0   160.5   153.7   168.9   191.7   179.3      5            1
  2026-04-19   195.6    207.2   171.6   181.0   160.5   153.7   168.9   191.7      6            1
  2026-04-20   249.8    195.6   207.2   171.6   181.0   160.5   153.7   168.9      0            0
  2026-04-21   156.3    249.8   195.6   207.2   171.6   181.0   160.5   153.7      1            0

Korelasi Pearson tiap fitur dengan target y:
  lag_1        +0.660
  lag_2        +0.468
  lag_3        +0.349
  lag_4        +0.332
  lag_5        +0.420
  lag_6        +0.598
  lag_7        +0.775
  hari         +0.473
  akhir_pekan  +0.543

Mari kita amati hasil transformasi tersebut:

  1. Baris 28–34 (pembangunan matriks): perulangan shift(k) membangun tujuh kolom lag, dua baris berikutnya menambahkan fitur kalender, dan dropna() memangkas tujuh baris pertama — 168 hari menjadi 161 baris supervised. Daftar fitur dikumpulkan agar nama kolom tidak ditulis ulang saat melatih model.
  2. Enam baris pertama (jendela yang bergeser): perhatikan diagonal nilai — lag_1 baris kedua berisi y baris pertama, lag_2 baris ketiga berisi y baris pertama, dan seterusnya. Satu nilai penjualan hadir di delapan kolom yang berbeda sepanjang pergeseran jendelanya; inilah wujud ketergantungan temporal dalam format tabular.
  3. Baris di sekitar 20 April (hari diskon kilat): target 249,8 pada 20 April berdiri di atas tujuh fitur lag yang seluruhnya normal (168–207). Tidak ada informasi di jendela lampau yang memberi tahu model tentang lonjakan tersebut — sebaliknya, baris 21 April membawa 249,8 sebagai lag_1, dan model harus belajar bahwa lompatan sebesar itu biasanya tidak berlanjut.
  4. Korelasi Pearson: lag_7 mencatat korelasi tertinggi (+0,775) — persis angka autokorelasi lag 7 pada Chapter Konsep Deret Waktu. Korelasi lag_1 (+0,660) dan penanda akhir_pekan (+0,543) menyusul. Urutan ini memberi dugaan awal fitur mana yang akan paling berpengaruh.

Melatih dan Mengevaluasi RandomForest #

Protokol evaluasi bab ini berbeda dari chapter ARIMA dan bedanya harus diucapkan secara eksplisit. Bab ini mengevaluasi prediksi satu langkah ke depan: ketika memprediksi hari t, nilai kemarin (lag_1) sudah diketahui karena berada di masa lalu. Chapter ARIMA meramalkan 14 hari sekaligus tanpa melihat satu pun nilai aktual di antaranya. Keduanya menjawab pertanyaan operasional yang berbeda — “berapa besok?” versus “berapa dua minggu ke depan?” — sehingga angka RMSE-nya tidak boleh dibandingkan mentah-mentah. Baseline pembandingnya pun menyesuaikan: naive versi satu-langkah cukup mengulang nilai kemarin.

python
# RandomForest pada fitur lag + kalender, dievaluasi satu langkah ke depan
# pada 14 hari terakhir dan dibandingkan dengan baseline naive (nilai kemarin).

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error


def buat_deret_penjualan(n_hari=168):
    rng = np.random.default_rng(42)
    idx = pd.date_range(start="2026-01-05", periods=n_hari, freq="D")
    t = np.arange(n_hari)
    tren = 120.0 + 0.45 * t
    hari = np.asarray(idx.dayofweek)
    efek_hari = np.select(
        [hari == 4, hari == 5, hari == 6],
        [10.0, 32.0, 40.0],
        default=0.0,
    )
    promo = ((t >= 70) & (t < 84)).astype(float) * 45.0
    kilat = (t == 105).astype(float) * 85.0
    derau = rng.normal(0.0, 12.0, n_hari)
    y = tren + efek_hari + promo + kilat + derau
    return pd.Series(np.round(y, 1), index=idx, name="penjualan")


penjualan = buat_deret_penjualan()

df = pd.DataFrame({"y": penjualan})
for k in range(1, 8):
    df[f"lag_{k}"] = penjualan.shift(k)
df["hari"] = penjualan.index.dayofweek
df["akhir_pekan"] = (penjualan.index.dayofweek >= 5).astype(int)
df = df.dropna()
fitur = [kolom for kolom in df.columns if kolom != "y"]

latih = df.iloc[:-14]      # sampai 2026-06-07 (147 baris)
uji = df.iloc[-14:]        # 2026-06-08 s.d. 2026-06-21
print(f"Data latih: {latih.index[0].date()} s.d. {latih.index[-1].date()} ({len(latih)} baris)")
print(f"Data uji  : {uji.index[0].date()} s.d. {uji.index[-1].date()} ({len(uji)} baris)")

model = RandomForestRegressor(n_estimators=300, random_state=42, n_jobs=1)
model.fit(latih[fitur], latih["y"])
prediksi = model.predict(uji[fitur])
naive = uji["lag_1"]       # prediksi satu-langkah: cukup ulang nilai kemarin

rmse_rf = np.sqrt(mean_squared_error(uji["y"], prediksi))
mae_rf = mean_absolute_error(uji["y"], prediksi)
rmse_naive = np.sqrt(mean_squared_error(uji["y"], naive))
mae_naive = mean_absolute_error(uji["y"], naive)
perbaikan = 100.0 * (rmse_naive - rmse_rf) / rmse_naive

print(f"\n{'model':28s} {'RMSE':>7s} {'MAE':>7s}")
print(f"{'RandomForest (lag + kalender)':28s} {rmse_rf:7.2f} {mae_rf:7.2f}")
print(f"{'naive (nilai kemarin)':28s} {rmse_naive:7.2f} {mae_naive:7.2f}")
print(f"Perbaikan RMSE RandomForest atas naive: {perbaikan:.1f}%")

print("\nPerbandingan aktual vs prediksi pada 14 hari data uji:")
for (tanggal, baris), pred in zip(uji.iterrows(), prediksi):
    print(f"  {tanggal.date()}  aktual {baris['y']:6.1f}   prediksi {pred:6.1f}   "
          f"naive {baris['lag_1']:6.1f}")

urutan = np.argsort(model.feature_importances_)[::-1]
print("\nKepentingan fitur (feature_importances_):")
for posisi in urutan:
    print(f"  {fitur[posisi]:12s} {model.feature_importances_[posisi]:.3f}")

Output yang Dihasilkan:

text
Data latih: 2026-01-12 s.d. 2026-06-07 (147 baris)
Data uji  : 2026-06-08 s.d. 2026-06-21 (14 baris)

model                           RMSE     MAE
RandomForest (lag + kalender)   15.00   13.18
naive (nilai kemarin)          25.09   21.61
Perbaikan RMSE RandomForest atas naive: 40.2%

Perbandingan aktual vs prediksi pada 14 hari data uji:
  2026-06-08  aktual  188.7   prediksi  193.7   naive  223.0
  2026-06-09  aktual  211.0   prediksi  186.9   naive  188.7
  2026-06-10  aktual  191.8   prediksi  198.0   naive  211.0
  2026-06-11  aktual  202.4   prediksi  191.4   naive  191.8
  2026-06-12  aktual  195.1   prediksi  203.9   naive  202.4
  2026-06-13  aktual  209.3   prediksi  220.3   naive  195.1
  2026-06-14  aktual  220.4   prediksi  227.5   naive  209.3
  2026-06-15  aktual  183.7   prediksi  194.7   naive  220.4
  2026-06-16  aktual  218.4   prediksi  191.7   naive  183.7
  2026-06-17  aktual  183.5   prediksi  195.9   naive  218.4
  2026-06-18  aktual  203.9   prediksi  188.7   naive  183.5
  2026-06-19  aktual  193.4   prediksi  204.1   naive  203.9
  2026-06-20  aktual  237.9   prediksi  210.7   naive  193.4
  2026-06-21  aktual  239.8   prediksi  231.8   naive  237.9

Kepentingan fitur (feature_importances_):
  lag_7        0.546
  lag_1        0.194
  lag_6        0.069
  lag_2        0.044
  hari         0.042
  lag_5        0.032
  lag_4        0.029
  lag_3        0.026
  akhir_pekan  0.019

Bedah kode di atas menyoroti empat kelompok baris:

  1. Baris 30–41 (matriks + pemisahan): pembangunan matriks disalin dari contoh pertama agar skrip mandiri, lalu 14 baris terakhir dipisahkan sebagai uji — jendela waktu yang sama dengan chapter ARIMA. RandomForestRegressor menerima random_state=42 dan n_jobs=1 demi hasil yang dapat direproduksi persis.
  2. Baris 43–52 (model dan baseline): model.fit() memakan 147 baris latih, predict() menghasilkan prediksi untuk 14 hari uji, dan kolom lag_1 langsung dipakai sebagai baseline naive satu-langkah. RMSE RandomForest 15,00 dan MAE 13,18 mengalahkan naive (25,09 dan 21,61) dengan perbaikan 40,2 persen — margin yang sehat, bukan kemenangan yang mencurigakan.
  3. Baris 59–62 (prediksi harian): pada akhir pekan 13–14 Juni, model memprediksi 220,3 dan 227,5 — pola Sabtu–Minggu berhasil dipelajari dari fitur. Galat terbesar justru muncul pada hari kerja yang tak lazim seperti 9 Juni (aktual 211,0, prediksi 186,9): persistensi mingguan tidak selalu menutup fluktuasi harian.
  4. Baris 64–67 (kepentingan fitur): lag_7 menyerap lebih dari separuh kepentingan (0,546), disusul lag_1 (0,194) — urutan yang persis mengikuti korelasi pada contoh pertama. Fitur kalender menyumbang kecil (0,042 dan 0,019) karena informasi hari sudah sebagian besar tersirat di lag_7; keduanya tetap berguna saat lag_7 berada jauh dari pola normalnya.
AspekSARIMA (bab sebelumnya)RandomForest fitur lag (bab ini)
Cara menangkap musimanOrde musiman seasonal_orderFitur lag_7 + fitur kalender
Fitur eksternalSulit (perlu exog yang sejajar)Bebas: kolom apa pun (penanda kampanye, cuaca, hari besar)
InterpretabilitasKoefisien + selang kepercayaanKepentingan fitur
Alatstatsmodels.tsa.arima.modelRegressor scikit-learn apa pun
Protokol alamiMulti-langkahSatu-langkah (multi-langkah perlu rekursif)

Dengan reframing ini, seluruh gudang alat supervised learning — dari regresi linear sampai gradient boosting — kini terbuka untuk data deret waktu. Namun satu pertanyaan evaluasi masih menggantung: seluruh angka pada bab ini berasal dari satu pemisahan latih-uji tunggal pada satu jendela waktu. Berapa kualitas model ini sepanjang musim lain — misalnya saat kampanye promo berlangsung dan model belum pernah melihatnya? Menjawabnya membutuhkan protokol evaluasi yang bergerak maju mengikuti waktu, dan itulah satu-satunya tema Chapter Validasi Walk-Forward.

Referensi Resmi #