Aturan alert dengan threshold statis dari chapter Metrics dan Alerting mudah dikalahkan oleh data yang punya pola: volume pesanan TokoKita naik tiap akhir pekan, dan lonjakan tanggal gajian terlihat seperti kebocoran data bagi aturan sederhana. Anomaly detection mengambil pendekatan berbeda: alih-alih bertanya “apakah nilai melewati batas tetap?”, kita bertanya “apakah nilai ini aneh dibandingkan perilaku data itu sendiri?”. Chapter ini menerapkan tiga metode statistik sederhana — z-score, IQR, dan rolling window — pada satu metrik harian yang sama, lalu membedah mengapa ketiganya bisa memberikan jawaban berbeda untuk anomali yang sama.

Tiga Metode Statistik Sederhana #

Ketiga metrik berikut memodelkan “perilaku normal” dengan cara berbeda:

  • z-score mengukur berapa simpangan sigma sebuah nilai dari rata-rata seluruh data; nilai dengan |z| > 3 dianggap anomali.
  • IQR memakai jarak antar-kuartil: nilai di luar [Q1 - 1.5*IQR, Q3 + 1.5*IQR] dianggap anomali. Kuartil tidak terpengaruh nilai ekstrem, sehingga metode ini kuat (robust).
  • Rolling window membandingkan setiap nilai dengan rata-rata dan simpangan baku dari beberapa periode sebelumnya, sehingga baseline ikut bergeser mengikuti tren.

Mengapa memulai dari metode statistik dan bukan model machine learning? Karena metode sederhana bisa dijelaskan kepada pemilik data dalam satu kalimat, dihitung dalam hitungan milidetik, dan gagalnya mudah didiagnosis. Model kompleks baru layak dipertimbangkan setelah metode sederhana terbukti kurang cukup — misalnya ketika pola musiman berlapis dengan tren panjang dan libur nasional.

Praktik pada Metrik Harian #

Kita bangun metrik jumlah_pesanan 28 hari dengan pola mingguan halus dan noise acak, lalu menyuntikkan dua anomali yang sudah diketahui: lonjakan di hari ke-19 (efek partisi terpasang ganda) dan penurunan di hari ke-26 (partisi kosong). Seed generator angka dikunci agar hasil identik di setiap eksekusi:

python
"""Deteksi anomali: z-score, IQR, dan rolling window pada metrik sintetis."""

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)

n = 28
indeks = pd.date_range("2026-09-14", periods=n, freq="D")
pola_mingguan = 1200 + 60 * np.sin(np.arange(n) * 2 * np.pi / 7)
pesanan = rng.normal(0, 45, n) + pola_mingguan

# Dua anomali yang sudah diketahui (ground truth).
pesanan[18] *= 2.1   # partisi terpasang ganda -> lonjakan
pesanan[25] *= 0.45  # partisi kosong -> penurunan

s = pd.Series(pesanan.round().astype(int), index=indeks, name="jumlah_pesanan")

# Metode 1: z-score global.
z = (s - s.mean()) / s.std(ddof=1)
flag_z = z.abs() > 3

# Metode 2: IQR.
q1, q3 = s.quantile(0.25), s.quantile(0.75)
iqr = q3 - q1
batas_bawah, batas_atas = q1 - 1.5 * iqr, q3 + 1.5 * iqr
flag_iqr = (s < batas_bawah) | (s > batas_atas)

# Metode 3: rolling window (pembanding = 7 hari sebelumnya).
jendela = s.shift(1).rolling(7)
z_rolling = (s - jendela.mean()) / jendela.std()
flag_rolling = z_rolling.abs() > 3

print("Sintetis 28 hari (seed=42): lonjakan di indeks 18, penurunan di indeks 25")
print()
print(f"z-score  : mean={s.mean():.1f} std={s.std(ddof=1):.1f}")
print(f"IQR      : Q1={q1:.0f} Q3={q3:.0f} batas=[{batas_bawah:.0f}, {batas_atas:.0f}]")
print()
print("== Hari terdeteksi anomali ==")
print(f"{'tanggal':<12}{'nilai':>7}{'z':>8}{'z_rolling':>11}  metode")
for tanggal in s.index:
    if flag_z[tanggal] or flag_iqr[tanggal] or flag_rolling[tanggal]:
        metode = []
        if flag_z[tanggal]:
            metode.append("z-score")
        if flag_iqr[tanggal]:
            metode.append("IQR")
        if flag_rolling[tanggal]:
            metode.append("rolling")
        print(f"{tanggal.strftime('%Y-%m-%d'):<12}{s[tanggal]:>7}"
              f"{z[tanggal]:>8.2f}{z_rolling[tanggal]:>11.2f}  {', '.join(metode)}")

print()
print("== Ringkasan per metode ==")
print(f"z-score global : {flag_z.sum()} hari")
print(f"IQR            : {flag_iqr.sum()} hari")
print(f"rolling 7 hari : {flag_rolling.sum()} hari")

Output yang Dihasilkan:

text
Sintetis 28 hari (seed=42): lonjakan di indeks 18, penurunan di indeks 25

z-score  : mean=1224.4 std=294.6
IQR      : Q1=1164 Q3=1248 batas=[1038, 1374]

== Hari terdeteksi anomali ==
tanggal       nilai       z  z_rolling  metode
2026-10-02     2548    4.49      33.94  z-score, IQR, rolling
2026-10-09      521   -2.39      -1.71  IQR

== Ringkasan per metode ==
z-score global : 1 hari
IQR            : 2 hari
rolling 7 hari : 1 hari

Mari kita telaah kode dan hasilnya — terutama ketidaksetujuan antar metode:

  1. Baris 14-15 (pesanan[18] *= 2.1): dua anomali disuntikkan pada indeks yang diketahui sehingga kita bisa menilai setiap metode terhadap ground truth, bukan menebak.
  2. Baris 20-21 (z-score): s.std(ddof=1) memakai simpangan baku sampel; seluruh 28 hari — termasuk hari anomali — ikut menghitung mean dan std, dan di sinilah letak kelemahannya.
  3. Baris 24-27 (IQR): kuartil dihitung dari posisi data terurut sehingga dua nilai ekstrem hampir tidak menggeser Q1 dan Q3; batas [1038, 1374] tetap rapat di sekitar data normal.
  4. Baris 30-32 (rolling): shift(1) memastikan hari yang dievaluasi tidak masuk baseline-nya sendiri; rolling(7) menghitung mean dan std dari tujuh hari sebelumnya.
  5. Lonjakan 2548 (2 Oktober): ketiga metode sepakat. Nilai z global 4.49 melewati ambang 3, dan z rolling mencapai 33.94 karena sebaran tujuh hari sebelumnya sangat kecil (~50), sehingga lonjakan 2.1 kali terasa ekstrem secara lokal.
  6. Penurunan 521 (9 Oktober): hanya IQR yang menandai. Z-score global menghitung penurunan ini “hanya” -2.39 karena lonjakan di awal bulan mengelembungkan std global menjadi 294.6 — padahal sebaran normal data hanya sekitar 45. Z rolling juga lolos karena jendela tujuh hari sebelum 9 Oktober masih memuat hari lonjakan, sehingga baseline ikut terkontaminasi.

Memilih Metode sesuai Karakter Data #

MetodeAsumsiKelebihanKeterbatasan
z-score globaldata berdistribusi normal dan stabilsederhana, mudah dijelaskanmean/std dirusak anomali itu sendiri
IQRsebagian besar data bersihrobust terhadap pencilanbutuh data cukup banyak untuk kuartil stabil
rolling windowtren berubah perlahanmengikuti pergeseran baselinejendela bisa terkontaminasi anomali sebelumnya

Ketiga keterbatasan di atas saling menutup: praktik umum memakai IQR atau rolling sebagai penyaring pertama, lalu z-score sebagai konfirmasi pada data yang sudah dibersihkan. Untuk data dengan tren naik panjang (pertumbuhan toko), rolling window adalah pilihan paling aman karena baseline terus menyesuaikan. Kombinasikan dengan aturan alert dari chapter sebelumnya: anomali terdeteksi menjadi sinyal P2 untuk diperiksa, sementara pelanggaran SLO yang sudah pasti tetap menjadi P1.

Musiman dan Baseline yang Tepat #

Data sintetis kita memuat pola mingguan, dan pola inilah jebakan terbesar deteksi sederhana: Jumat yang sibuk bisa terdeteksi “anomali” oleh baseline yang dihitung lintas hari kerja dan akhir pekan sekaligus. Dua perbaikan lazim dilakukan: membandingkan nilai dengan hari yang sama minggu sebelumnya (week-over-week), atau memakai baseline yang menghapus komponen musiman terlebih dahulu (dekomposisi musiman). Aturan praktisnya: sebelum menyetir ambang deteksi, plot dulu data beberapa periode penuh — pola yang terlihat mata biasanya menentukan pilihan metode. Untuk kebutuhan lanjutan, pustaka seperti statsmodels menyediakan dekomposisi STL; operasi rolling di pandas didokumentasikan di https://pandas.pydata.org/docs/reference/api/pandas.Series.rolling.html .

Deteksi anomali menjaga metrik proses tetap sehat. Namun anomali bisa juga bersembunyi di dalam isi tabel — nilai kolom yang bergeser, baris ganda, rentang yang dilanggar — dan untuk itu kita butuh pustaka validasi yang purpose-built. Chapter berikutnya membahas yang paling populer: Great Expectations.