Autokorelasi lag 7 pada chapter sebelumnya memberitahu kita bahwa penjualan Toko Ceria memuat pola mingguan — tetapi pola itu masih tercampur dengan tren dan lonjakan kampanye di dalam satu deret. Dekomposisi deret waktu (time series decomposition) membedah deret menjadi tiga komponen yang lebih mudah dibaca: tren, musiman, dan residu. Hasil pembedahan ini bermanfaat untuk diagnosis (“apakah penjualan naik karena tren atau musiman?”), untuk membangun fitur bagi model, dan untuk menandai hari-hari tak wajar melalui residu yang besar. Kita akan memakai fungsi seasonal_decompose dari statsmodels pada data yang sama dengan chapter sebelumnya, lalu menghadapi variasi kedua: deret yang amplitudo musimannya tumbuh seiring level.

Dua Model Dekomposisi: Aditif dan Multiplikatif #

Model aditif menuliskan deret sebagai penjumlahan tiga komponen: y_t = T_t + S_t + R_t, dengan T_t tren, S_t komponen musiman, dan R_t residu. Model ini cocok ketika besar lonjakan musiman relatif tetap dari waktu ke waktu — misalnya akhir pekan selalu menambah sekitar 40 unit penjualan, baik saat toko baru buka maupun enam bulan kemudian. Model multiplikatif menuliskan deret sebagai hasil kali: y_t = T_t x S_t x R_t. Model kedua cocok ketika lonjakan musiman tumbuh seiring level: toko yang penjualannya menggandakan diri biasanya juga menggandakan lonjakan akhir pekannya, karena basis pembelinya yang membesar.

AspekModel aditifModel multiplikatif
Bentuk relasiy_t = T_t + S_t + R_ty_t = T_t x S_t x R_t
Pola musimanAmplitudo tetap (unit)Amplitudo sebanding level (persentase)
Komponen musimanNilai plus/minus dalam unitFaktor pengali (sekitar 1,0)
Contoh khasPola jam sibuk harian yang stabilRitel yang tumbuh pesat, inflasi harga
Jembatan antar-model—Transformasi log mengubahnya menjadi aditif

Dekomposisi Aditif pada Penjualan Toko Ceria #

Fungsi seasonal_decompose menerima deret dengan frekuensi teratur dan satu parameter wajib kita isi: period, yaitu panjang satu siklus musiman — tujuh hari untuk pola mingguan. Parameter ini menggantikan nama freq yang sudah tidak digunakan lagi pada versi statsmodels modern, jadi biasakan menulis period=7, bukan freq=7. Contoh berikut mendekomposisi 168 hari penjualan Toko Ceria, lalu membaca ketiga komponen dari atribut hasil: .trend, .seasonal, dan .resid.

python
# Dekomposisi aditif: membedah deret penjualan Toko Ceria menjadi
# tren, komponen musiman mingguan, dan residu dengan statsmodels.

import numpy as np
import pandas as pd
from statsmodels.tsa.seasonal import seasonal_decompose


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()
hasil = seasonal_decompose(penjualan, model="additive", period=7)

tren_bersih = hasil.trend.dropna()
print(f"Nilai tren NaN di awal dan akhir deret: "
      f"{int(hasil.trend.iloc[:3].isna().sum())} dan {int(hasil.trend.iloc[-3:].isna().sum())} baris")
print(f"Tren awal  ({tren_bersih.index[0].date()}): {tren_bersih.iloc[0]:8.2f}")
print(f"Tren akhir ({tren_bersih.index[-1].date()}): {tren_bersih.iloc[-1]:8.2f}")

sebelum = tren_bersih.loc["2026-03-08"]
tengah = tren_bersih.loc["2026-03-22"]
sesudah = tren_bersih.loc["2026-04-05"]
lonjakan = tengah - 0.5 * (sebelum + sesudah)
print(f"Lonjakan tren di tengah kampanye (di atas garis lurus): {lonjakan:+.2f}")

nama_hari = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"]
print("\nKomponen musiman mingguan (unit):")
for nama, nilai in zip(nama_hari, hasil.seasonal.iloc[:7]):
    print(f"  {nama:8s} {nilai:+7.2f}")
print(f"  Amplitudo musiman: {hasil.seasonal.max() - hasil.seasonal.min():.2f} unit")

residu = hasil.resid
masker_luar = (residu.index < pd.Timestamp("2026-03-16")) | (residu.index > pd.Timestamp("2026-03-29"))
masker_luar &= residu.index != pd.Timestamp("2026-04-20")
print(f"\nResidu: rata-rata {residu.mean():+.3f}, simpangan baku {residu.std():.2f}")
print(f"Residu pada hari diskon kilat (2026-04-20): {residu.loc['2026-04-20']:+.2f}")
print(f"Residu rata-rata selama kampanye 16-29 Mar: "
      f"{residu.loc['2026-03-16':'2026-03-29'].mean():+.2f}")
print(f"Residu rata-rata di luar kampanye & diskon kilat: {residu[masker_luar].mean():+.2f}")

Output yang Dihasilkan:

text
Nilai tren NaN di awal dan akhir deret: 3 dan 3 baris
Tren awal  (2026-01-08):   129.34
Tren akhir (2026-06-18):   208.66
Lonjakan tren di tengah kampanye (di atas garis lurus): +45.46

Komponen musiman mingguan (unit):
  Senin      -8.60
  Selasa    -11.59
  Rabu      -14.45
  Kamis     -11.08
  Jumat      -4.49
  Sabtu     +19.49
  Minggu    +30.71
  Amplitudo musiman: 45.16 unit

Residu: rata-rata +0.002, simpangan baku 11.56
Residu pada hari diskon kilat (2026-04-20): +69.36
Residu rata-rata selama kampanye 16-29 Mar: +4.64
Residu rata-rata di luar kampanye & diskon kilat: -0.91

Mari kita telaah baris-baris kode yang berperan penting:

  1. Baris 27–28 (seasonal_decompose(...)): pemanggilan dengan model="additive" dan period=7 mengembalikan objek hasil yang menyimpan tiga komponen sebagai pandas.Series berindeks sama dengan deret asal, sehingga komponen dapat dipotong dan dibandingkan per tanggal.
  2. Baris 30–34 (hasil.trend): tren dihitung dengan rata-rata bergerak dua sisi (two-sided) sepanjang satu periode, sehingga tiga hari pertama dan terakhir bernilai NaN — belum ada cukup tetangga di kedua sisi. dropna() membuang tepi tersebut sebelum dibaca.
  3. Baris 36–40 (uji lonjakan tren): nilai tren pada 8 Maret dan 5 April dihubungkan garis lurus, lalu tren 22 Maret dibandingkan terhadap titik tengah garis tersebut. Lonjakan +45,46 hampir persis sebesar suntikan kampanye (+45): perubahan level selama dua minggu penuh diserap komponen tren, bukan residu.
  4. Baris 42–46 (hasil.seasonal): komponen musiman berulang setiap tujuh hari, jadi tujuh nilai pertama merangkum seluruh pola. Hari kerja bernilai negatif dan akhir pekan positif dengan Minggu paling tinggi (+30,71) — amplitudo 45,16 unit mencerminkan jarak hari paling sepi ke paling ramai.
  5. Baris 48–55 (hasil.resid): residu adalah sisa setelah tren dan musiman dikurangkan. Rata-ratanya +0,002 — praktis nol, sifat yang diharapkan dari komponen sisa. Baris 51–52 menunjuk hari diskon kilat 20 April: residu +69,36 jauh melampaui simpangan baku 11,56, sehingga hari itu otomatis menonjol sebagai kandidat anomali.

Ketika Amplitudo Musiman Tumbuh: Model Multiplikatif #

Kategori produk yang sedang tumbuh pesat di Toko Ceria memberi contoh lawan dari deret pertama: level naik hampir tiga kali lipat dalam enam bulan dan lonjakan akhir pekan ikut membesar. Pada deret seperti ini, model aditif memaksakan amplitudo musiman yang tetap, sehingga pola yang tidak tertangkap menumpuk di residu. Contoh berikut membangun deret multiplikatif secara eksplisit, lalu membandingkan kedua model pada deret yang sama.

python
# Dekomposisi multiplikatif untuk deret yang amplitudo musimannya tumbuh
# seiring level: kategori produk yang sedang tumbuh pesat di Toko Ceria.

import numpy as np
import pandas as pd
from statsmodels.tsa.seasonal import seasonal_decompose

rng = np.random.default_rng(42)
n_hari = 168
idx = pd.date_range(start="2026-01-05", periods=n_hari, freq="D")
t = np.arange(n_hari)
hari = np.asarray(idx.dayofweek)

# Level tumbuh eksponensial dan lonjakan akhir pekan ikut membesar.
level = 120.0 * (1.0 + 0.006) ** t
faktor_hari = np.select([hari == 5, hari == 6], [1.28, 1.35], default=1.0)
derau_persen = rng.normal(1.0, 0.05, n_hari)
y = level * faktor_hari * derau_persen
penjualan = pd.Series(np.round(y, 2), index=idx, name="penjualan")

print(f"Rata-rata minggu pertama : {penjualan.iloc[:7].mean():7.2f}")
print(f"Rata-rata minggu terakhir : {penjualan.iloc[-7:].mean():7.2f}")
print(f"Selisih maksimum-minimum minggu pertama : {penjualan.iloc[:7].max() - penjualan.iloc[:7].min():7.2f}")
print(f"Selisih maksimum-minimum minggu terakhir : {penjualan.iloc[-7:].max() - penjualan.iloc[-7:].min():7.2f}")

hasil_mul = seasonal_decompose(penjualan, model="multiplicative", period=7)
hasil_add = seasonal_decompose(penjualan, model="additive", period=7)

nama_hari = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"]
print("\nFaktor musiman mingguan (model multiplikatif):")
for nama, nilai in zip(nama_hari, hasil_mul.seasonal.iloc[:7]):
    print(f"  {nama:8s} {nilai:6.3f}")

print(f"\nResidu multiplikatif: rata-rata {hasil_mul.resid.mean():.4f}, "
      f"simpangan baku {hasil_mul.resid.std():.4f}")
print(f"Residu aditif       : rata-rata {hasil_add.resid.mean():+.2f}, "
      f"simpangan baku {hasil_add.resid.std():.2f}")
rerata = penjualan.mean()
print(f"\nSimpangan baku residu relatif thd. rata-rata deret:")
print(f"  multiplikatif: {hasil_mul.resid.std():.4f}")
print(f"  aditif       : {hasil_add.resid.std() / rerata:.4f}")

Output yang Dihasilkan:

text
Rata-rata minggu pertama :  131.15
Rata-rata minggu terakhir :  354.47
Selisih maksimum-minimum minggu pertama :   58.07
Selisih maksimum-minimum minggu terakhir :  145.41

Faktor musiman mingguan (model multiplikatif):
  Senin     0.917
  Selasa    0.921
  Rabu      0.909
  Kamis     0.920
  Jumat     0.907
  Sabtu     1.172
  Minggu    1.253

Residu multiplikatif: rata-rata 0.9999, simpangan baku 0.0396
Residu aditif       : rata-rata -0.12, simpangan baku 12.65

Simpangan baku residu relatif thd. rata-rata deret:
  multiplikatif: 0.0396
  aditif       : 0.0564

Bedah kode di atas menyoroti empat kelompok baris:

  1. Baris 8–19 (pembangunan deret): level tumbuh 0,6 persen per hari secara eksponensial, faktor akhir pekan 1,28 dan 1,35 mengalikan level (bukan menambah), dan derau berbentuk persentase (rata-rata 1,0, simpangan 0,05). Seluruh komponen bergabung lewat perkalian — definisi operasional deret multiplikatif.
  2. Baris 21–24 (diagnosis amplitudo): rata-rata mingguan naik dari 131,15 ke 354,47, dan rentang dalam satu minggu melebar dari 58,07 ke 145,41. Amplitudo yang tumbuh seiring level adalah petunjuk visual terkuat bahwa model aditif tidak lagi cocok.
  3. Baris 29–32 (faktor musiman): komponen musiman model multiplikatif berupa faktor pengali relatif terhadap tren. Hari kerja berkumpul di sekitar 0,91, Sabtu 1,172, dan Minggu 1,253 — perbedaan kecil antar hari kerja mencerminkan derau, bukan pola.
  4. Baris 34–41 (adu model): residu model multiplikatif bersimpangan baku 0,0396 (rata-rata 0,9999 — residu multiplikatif idealnya berpusat di 1,0). Dibandingkan pada skala relatif terhadap rata-rata deret, residu model aditif (0,0564) sekitar 42 persen lebih besar daripada model multiplikatif (0,0396) — model yang salah bentuk meninggalkan jejak pada residu.

Dekomposisi memberi kita kacamata untuk membaca struktur deret, dan salah satu temuannya sangat menentukan bagi chapter berikutnya: tren membuat rata-rata penjualan terus bergerak, dari 129 ke 209 dalam enam bulan. Model ARIMA klasik tidak siap bekerja pada deret yang rata-ratanya berjalan — deret harus dibuat stationary lebih dulu, dan kestabilan itu perlu diuji, bukan dikira. Kedua hal tersebut adalah materi Chapter Stationaritas dan Uji Augmented Dickey-Fuller.

Referensi Resmi #