Chapter sebelumnya meninggalkan satu kesiapan penting: deret diferensi penjualan Toko Ceria telah lulus uji ADF dan KPSS, sehingga bahan baku untuk pemodelan statistik klasik sudah tersedia. ARIMA (AutoRegressive Integrated Moving Average) adalah model yang mengolah bahan tersebut — selama setengah abad menjadi standar de facto forecasting sebelum metode machine learning modern hadir. Kita akan membongkar makna ketiga hurufnya, memilih orde lewat nilai numerik ACF dan PACF, lalu memasang beberapa kandidat dari statsmodels dan mengadunya secara jujur dengan baseline naive pada 14 hari terakhir yang disisihkan sebagai data uji.

Anatomi ARIMA: p, d, dan q #

Ketiga huruf pada ARIMA merujuk pada tiga komponen yang bekerja di atas deret hasil differencing. Komponen AR(p) (autoregressive) menyatakan bahwa nilai sekarang adalah kombinasi linear dari p nilai lampau: y'_t = c + phi_1*y'_(t-1) + ... + phi_p*y'_(t-p). Komponen I(d) (integrated) adalah jumlah kali differencing — d = 1 pada kasus kita karena satu kali selisih sudah menstabilkan rata-rata. Komponen MA(q) (moving average) menyatakan bahwa nilai sekarang dipengaruhi q guncangan lampau: y'_t = c + eps_t + theta_1*eps_(t-1) + ... + theta_q*eps_(t-q), dengan eps adalah residu model pada masa lalu.

KomponenYang dimodelkanOrdePetunjuk pemilihan orde
AR(p)Dependensi pada nilai lampaupPACF terpotong setelah lag p
I(d)Differencing penstabil rata-ratadUji ADF/KPSS pada tiap tingkat diferensi
MA(q)Dependensi pada guncangan lampauqACF terpotong setelah lag q

Memilih Orde dengan ACF dan PACF #

Fungsi ACF (autocorrelation function) menghitung autokorelasi pada setiap lag, sedangkan PACF (partial autocorrelation function) menghitung korelasi pada satu lag setelah pengaruh lag-lag di antaranya dikeluarkan. Aturan praktisnya: PACF yang terpotong setelah lag p menunjuk komponen AR(p); ACF yang terpotong setelah lag q menunjuk komponen MA(q). Keduanya dihitung pada deret yang sudah didiferensikan — level yang sama dengan yang akan dilihat oleh model. Sebagai patokan signifikansi, nilai autokorelasi di luar kisaran +-1.96/sqrt(n) dianggap bukan kebetulan. Fungsi acf dan pacf dari statsmodels.tsa.stattools mengembalikan larik angka, jadi kita dapat membacanya tanpa membuat grafik.

python
# ACF dan PACF pada deret diferensi data latih: membaca nilai numeriknya
# untuk memilih orde p dan q pada ARIMA, tanpa membuat grafik.

import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import acf, pacf


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()
latih = penjualan.iloc[:154]          # 14 hari terakhir disisihkan sebagai uji
diferensi = np.diff(latih.to_numpy())
n = len(diferensi)
batas = 1.96 / np.sqrt(n)

nilai_acf = acf(diferensi, nlags=10)
nilai_pacf = pacf(diferensi, nlags=10)

print(f"Data latih : {latih.index[0].date()} s.d. {latih.index[-1].date()} ({len(latih)} hari)")
print(f"Observasi diferensi: {n}   batas signifikansi: +/-{batas:.3f}")
print("\nlag      ACF      PACF   (* = melewati batas)")
for lag in range(1, 11):
    tanda_acf = "*" if abs(nilai_acf[lag]) > batas else " "
    tanda_pacf = "*" if abs(nilai_pacf[lag]) > batas else " "
    print(f"{lag:3d}  {nilai_acf[lag]:+6.3f}{tanda_acf}  {nilai_pacf[lag]:+6.3f}{tanda_pacf}")

Output yang Dihasilkan:

text
Data latih : 2026-01-05 s.d. 2026-06-07 (154 hari)
Observasi diferensi: 153   batas signifikansi: +/-0.158

lag      ACF      PACF   (* = melewati batas)
  1  -0.180*  -0.181*
  2  -0.151   -0.193*
  3  -0.119   -0.204*
  4  -0.166*  -0.308*
  5  -0.169*  -0.453*
  6  +0.045   -0.484*
  7  +0.519*  +0.165*
  8  +0.064   +0.284*
  9  -0.207*  +0.032
 10  -0.090   -0.027

Mari kita baca hasilnya baris demi hasil:

  1. Baris 27–31 (persiapan): deret dipotong kronologis — 154 hari pertama untuk latih, 14 hari terakhir untuk uji — lalu didiferensikan. Batas signifikansi 1.96/sqrt(153) = 0,158 menjadi pembanding seluruh nilai pada tabel.
  2. Lag 1 (ACF −0,180, PACF −0,181): keduanya negatif dan melewati batas — sisa ketergantungan jangka pendek pada deret diferensi. Konfigurasi ini cocok untuk MA(1) dengan koefisien negatif maupun AR(1) kecil; keduanya layak dicoba pada orde rendah.
  3. Lag 7 (ACF +0,519): lonjakan jauh melewati batas pada kelipatan tujuh adalah tanda tegas musiman mingguan yang belum tertangani. ARIMA non-musiman hanya punya AR/MA pada lag 1–2, sehingga lag 7 ini akan bocor ke residu — petunjuk bahwa kita membutuhkan komponen musiman.
  4. Lag 2–6 pada PACF yang makin negatif: pola menurun tersebut adalah pantulan dari struktur musiman mingguan, bukan undangan menaikkan p sampai 6 — menambah banyak parameter akan membebani model dengan 154 observasi saja.

Merakit dan Mengevaluasi ARIMA #

Berdasarkan pembacaan tabel ACF/PACF di atas, kita memasang empat kandidat: tiga ARIMA non-musiman dengan kompleksitas bertingkat, plus satu varian musiman SARIMA yang menambahkan orde AR dan MA pada lag 7 lewat parameter seasonal_order=(P, D, Q, s) dengan s = 7. Semua model dilatih pada 154 hari pertama, meramalkan 14 hari sekaligus, lalu dinilai dengan RMSE terhadap nilai sebenarnya — protokol multi-langkah yang berbeda dari evaluasi satu-langkah pada chapter berikutnya. Baseline pembandingnya adalah naive: nilai hari terakhir latih diulang 14 kali.

python
# ARIMA vs SARIMA untuk meramalkan 14 hari penjualan Toko Ceria:
# dibandingkan dengan baseline naive dan dievaluasi pada data uji kronologis.

import warnings

import numpy as np
import pandas as pd
from sklearn.metrics import mean_squared_error
from statsmodels.tsa.arima.model import ARIMA


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()
latih = penjualan.iloc[:154]
uji = penjualan.iloc[154:]

naive = np.full(len(uji), latih.iloc[-1])
rmse_naive = np.sqrt(mean_squared_error(uji.to_numpy(), naive))
print(f"Baseline naive (nilai terakhir diulang): RMSE = {rmse_naive:.2f}")

kandidat = [
    ("ARIMA(0,1,1)", dict(order=(0, 1, 1))),
    ("ARIMA(1,1,1)", dict(order=(1, 1, 1))),
    ("ARIMA(2,1,2)", dict(order=(2, 1, 2))),
    ("SARIMA(1,1,1)(1,0,1)7", dict(order=(1, 1, 1), seasonal_order=(1, 0, 1, 7))),
]

# Peringatan optimasi numerik statsmodels tidak mengubah hasil; saring agar keluaran bersih.
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    print(f"\n{'model':24s} {'AIC':>9s} {'RMSE uji':>9s}")
    hasil_model = {}
    for nama, kwargs in kandidat:
        res = ARIMA(latih, **kwargs).fit()
        ramalan = res.forecast(steps=len(uji))
        rmse = np.sqrt(mean_squared_error(uji.to_numpy(), ramalan.to_numpy()))
        hasil_model[nama] = res
        print(f"{nama:24s} {res.aic:9.2f} {rmse:9.2f}")

terpilih = hasil_model["SARIMA(1,1,1)(1,0,1)7"]
interval = terpilih.conf_int()
print("\nParameter SARIMA terpilih (taksiran dan selang kepercayaan 95%):")
for nama_param, taksiran in terpilih.params.items():
    lo, hi = interval.loc[nama_param]
    print(f"  {nama_param:10s} {taksiran:+8.3f}   [{lo:+7.3f}, {hi:+7.3f}]")

ramalan = terpilih.get_forecast(steps=len(uji))
nilai_ramalan = ramalan.predicted_mean
batas_ramalan = ramalan.conf_int()
print(f"\nRamalan 14 hari ({nilai_ramalan.index[0].date()} s.d. {nilai_ramalan.index[-1].date()}):")
for tanggal, pred in nilai_ramalan.items():
    lo = batas_ramalan.loc[tanggal].iloc[0]
    hi = batas_ramalan.loc[tanggal].iloc[1]
    print(f"  {tanggal.date()}  {pred:7.1f}   [{lo:6.1f}, {hi:6.1f}]")

Output yang Dihasilkan:

text
Baseline naive (nilai terakhir diulang): RMSE = 24.73

model                          AIC  RMSE uji
ARIMA(0,1,1)               1410.21     18.66
ARIMA(1,1,1)               1390.76     20.00
ARIMA(2,1,2)               1393.44     19.99
SARIMA(1,1,1)(1,0,1)7      1296.85     17.40

Parameter SARIMA terpilih (taksiran dan selang kepercayaan 95%):
  ar.L1        -0.043   [ -0.281,  +0.194]
  ma.L1        -0.607   [ -0.823,  -0.392]
  ar.S.L7      +1.000   [ +0.994,  +1.006]
  ma.S.L7      -0.993   [ -2.319,  +0.333]
  sigma2     +223.606   [-58.392, +505.605]

Ramalan 14 hari (2026-06-08 s.d. 2026-06-21):
  2026-06-08    183.8   [ 154.0,  213.7]
  2026-06-09    178.1   [ 146.5,  209.7]
  2026-06-10    179.2   [ 145.6,  212.8]
  2026-06-11    181.2   [ 145.8,  216.6]
  2026-06-12    188.9   [ 151.8,  226.1]
  2026-06-13    213.4   [ 174.6,  252.2]
  2026-06-14    224.7   [ 184.3,  265.1]
  2026-06-15    186.6   [ 144.3,  228.9]
  2026-06-16    180.8   [ 136.9,  224.7]
  2026-06-17    181.9   [ 136.5,  227.3]
  2026-06-18    184.0   [ 137.0,  230.9]
  2026-06-19    191.6   [ 143.3,  240.0]
  2026-06-20    216.1   [ 166.4,  265.9]
  2026-06-21    227.4   [ 176.3,  278.5]

Bedah kode di atas menyoroti empat kelompok baris:

  1. Baris 30–36 (baseline naive): sebelum percaya pada model apa pun, nilai terakhir data latih diulang sebagai ramalan. RMSE 24,73 adalah ambang minimal yang harus dikalahkan — cara yang sama seperti kebiasaan kita menetapkan baseline sejak chapter awal Course ini.
  2. Baris 38–55 (adu kandidat): keempat model dipasang dengan pola kerja yang sama: ARIMA(...) menerima deret dan order (plus seasonal_order untuk varian musiman), .fit() memperkirakan parameter, dan .forecast(steps=14) meramalkan 14 hari sekaligus. Blok warnings pada baris 46–47 menyaring peringatan optimasi numerik yang tidak mengubah hasil. SARIMA menang telak: AIC 1296,85 dan RMSE 17,40 — naik dari naive 24,73 berarti galat berkurang sekitar 30 persen.
  3. Baris 57–62 (parameter dan selang kepercayaan): res.params menyimpan taksiran tiap koefisien dan res.conf_int() selang kepercayaan 95%-nya. Nilai ar.L1 −0,043 dengan selang yang memotong nol menandakan komponen AR(1) nyaris tidak dibutuhkan — wajar, karena differencing sudah menyita ketergantungan jangka pendek. Koefisien ar.S.L7 +1,000 dengan selang sempit menegaskan musiman mingguan yang sangat kuat; sigma2 223,6 berarti residu model bersimpangan sekitar 15 unit per hari. Selang ma.S.L7 yang lebar adalah pengingat jujur bahwa 154 observasi membatasi ketepatan taksiran.
  4. Baris 64–71 (ramalan + interval): get_forecast() menghasilkan rata-rata ramalan sekaligus selang kepercayaan per tanggal. Pola mingguan terlihat nyata pada angka ramalan: hari kerja berkisar 178–192, lalu Sabtu 213,4 dan Minggu 224,7 — model berhasil menurunkan pola akhir pekan ke masa depan. Selang kepercayaan melebar seiring horizon, dari sekitar 60 unit pada hari pertama menjadi lebih dari 100 unit pada hari ke-14: ketidakpastian yang jujur meningkat makin jauh kita meramal.

SARIMA pada akhirnya hanyalah ARIMA yang diperluas: kelas ARIMA yang sama, ditambah seasonal_order. Pesan pentingnya bukan “selalu pakai SARIMA”, melainkan “komponen musiman harus masuk ke model dalam bentuk apa pun”. Model statistik adalah satu jalur; jalur kedua membawa pola deret waktu ke dalam kerangka supervised learning yang sudah kita kuasai sejak awal Course — mengubah deret menjadi matriks fitur lag dan kalender, lalu membiarkan RandomForest belajar dari pola tersebut. Transformasi itulah yang kita bangun pada Chapter Forecasting dengan Fitur Lag.

Referensi Resmi #