İçeriğe geç

Hands-on Lab: NYC Taxi Talep Anomalisinde 5 İstatistiksel Detektör Karşılaştırma

Numenta NAB benchmark'ından NYC Taxi saatlik talep verisi: z-score, modified z, IQR, adjusted boxplot ve POT detektörlerini yan yana koşturup PR-AUC karşılaştırması — kursun ilk gerçek dataset hands-on lab'ı.

Şükrü Yusuf KAYA
45 dakikalık okuma
Orta
Hands-on Lab: NYC Taxi Talep Anomalisinde 5 İstatistiksel Detektör Karşılaştırma
🚕 İlk gerçek veri lab'ı
Sentetikten gerçeğe geçiş. Bu lab'da Numenta NAB benchmark'ından NYC Taxi verisini kullanacağız: saatlik taksi yolcu sayısı, ~10K nokta, gerçek olaylarla etiketli anomaliler (NYC Maratonu, Thanksgiving, blizzard, vb.). Modül 2'de öğrendiğin 5 istatistiksel detektörü uygulayıp PR-AUC tablosu çıkaracağız. Sonunda 'hangi yöntem hangi koşulda parlar' sezgine sahip olacaksın.

Lab Hazırlığı#

Modül 0.4'te NAB veri indirme komutunu vermiştik. Önce onu çek (zaten indirdiysen atla):
cd ~/projeler/anomaly-detection mkdir -p data/raw/numenta cd data/raw/numenta # Numenta NAB GitHub'dan klonla git clone --depth=1 https://github.com/numenta/NAB.git # NYC Taxi verisi ls NAB/data/realKnownCause/ # nyc_taxi.csv görmelisin # Etiketler (anomaly window'lar) cat NAB/labels/combined_windows.json | head -30
Şimdi notebook'umuzu açalım:
cd ~/projeler/anomaly-detection jupyter lab notebooks/02-nyc-taxi-benchmark.ipynb

Adım 1: Veri Yükleme ve İnceleme#

python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import json
 
DATA_PATH = '../data/raw/numenta/NAB/data/realKnownCause/nyc_taxi.csv'
LABELS_PATH = '../data/raw/numenta/NAB/labels/combined_windows.json'
 
# Veriyi yükle
df = pd.read_csv(DATA_PATH, parse_dates=['timestamp'])
df = df.rename(columns={'value': 'passengers'})
 
print(f"Şekil: {df.shape}")
print(f"Zaman aralığı: {df['timestamp'].min()} → {df['timestamp'].max()}")
print(f"Frekans: {(df['timestamp'].iloc[1] - df['timestamp'].iloc[0])}")
df.head()
NYC Taxi verisi yükleme
python
# Anomaly window'ları yükle
with open(LABELS_PATH) as f:
all_labels = json.load(f)
 
nyc_windows = all_labels['realKnownCause/nyc_taxi.csv']
print(f"NYC Taxi anomaly window sayısı: {len(nyc_windows)}")
for w in nyc_windows:
print(f" {w[0]} → {w[1]}")
 
# Her satır için 'is_anomaly' etiketi üret
df['is_anomaly'] = 0
for start, end in nyc_windows:
mask = (df['timestamp'] >= start) & (df['timestamp'] <= end)
df.loc[mask, 'is_anomaly'] = 1
 
print(f"\nAnomaly satır sayısı: {df['is_anomaly'].sum()}")
print(f"Anomaly oranı: {df['is_anomaly'].mean()*100:.2f}%")
Anomaly etiketlerini yükle

Adım 2: EDA — Veriye Bak#

python
fig, ax = plt.subplots(figsize=(15, 5))
ax.plot(df['timestamp'], df['passengers'], color='steelblue',
linewidth=0.5, alpha=0.7)
 
# Anomaly window'ları gölgele
for start, end in nyc_windows:
ax.axvspan(start, end, color='crimson', alpha=0.2)
 
ax.set_title('NYC Taxi — Saatlik Yolcu Sayısı (kırmızı bölgeler anomaly)', fontsize=14)
ax.set_xlabel('Zaman')
ax.set_ylabel('Yolcu sayısı')
plt.tight_layout()
plt.savefig('reports/nyc_taxi_overview.png', dpi=120)
plt.show()
 
# İstatistikler
print(df['passengers'].describe())
NYC Taxi zaman serisi görselleştirme
👁️ Veriyi gözle anla
Çıkardığın grafikte 5 anomaly window var. Üçü açıkça görünüyor (düşüşler veya yükselişler): NYC Maratonu, Thanksgiving, blizzard, yeni yıl. İki tanesi daha gizli. Anomaly'nin tipik bir özelliği: her zaman 'göze çarpıcı' olmaz. Bu yüzden modeli koşturuyoruz — eldeki insanlık değil.

Adım 3: Detektör Fonksiyonlarını Tanımla#

Modül 2.1-2.5'te yazdığımız fonksiyonların production-ready versiyonları:
python
from scipy.stats import genpareto
from statsmodels.stats.stattools import medcouple
 
def detector_zscore(x, threshold=3.0):
"""Klasik z-score detector."""
z = (x - x.mean()) / (x.std(ddof=0) + 1e-9)
return np.abs(z), np.abs(z) > threshold
 
def detector_modified_z(x, threshold=3.5):
"""Modified z-score (MAD-based)."""
med = np.median(x)
mad = np.median(np.abs(x - med))
if mad == 0:
return np.zeros_like(x), np.zeros_like(x, dtype=bool)
m = 0.6745 * (x - med) / mad
return np.abs(m), np.abs(m) > threshold
 
def detector_iqr(x, k=1.5):
"""IQR / Tukey's fences detector."""
q1, q3 = np.percentile(x, [25, 75])
iqr = q3 - q1
lower, upper = q1 - k * iqr, q3 + k * iqr
# Skor: sınırdan ne kadar uzakta?
score = np.maximum(np.maximum(lower - x, 0), np.maximum(x - upper, 0))
return score, (x < lower) | (x > upper)
 
def detector_adjusted_boxplot(x, k=1.5):
"""Hubert-Vandervieren adjusted boxplot."""
q1, q3 = np.percentile(x, [25, 75])
iqr = q3 - q1
mc = medcouple(x)
if mc >= 0:
lower = q1 - k * np.exp(-4 * mc) * iqr
upper = q3 + k * np.exp(3 * mc) * iqr
else:
lower = q1 - k * np.exp(-3 * mc) * iqr
upper = q3 + k * np.exp(4 * mc) * iqr
score = np.maximum(np.maximum(lower - x, 0), np.maximum(x - upper, 0))
return score, (x < lower) | (x > upper)
 
def detector_pot(x, threshold_percentile=95, anomaly_q=0.001):
"""Peak Over Threshold with GPD."""
u = np.percentile(x, threshold_percentile)
excess = x[x > u] - u
if len(excess) < 30:
return np.zeros_like(x, dtype=float), np.zeros_like(x, dtype=bool)
shape, _, scale = genpareto.fit(excess, floc=0)
 
# Anomaly eşiği (VaR formulu)
n, Nu = len(x), len(excess)
if abs(shape) < 1e-6:
var_th = u - scale * np.log((n / Nu) * anomaly_q)
else:
var_th = u + (scale / shape) * (((n / Nu) * anomaly_q) ** (-shape) - 1)
 
# Skor: eşiği aşma miktarı
score = np.maximum(x - u, 0)
return score, x > var_th
5 detektör fonksiyonu

Adım 4: Hepsini Koştur, Metriği Topla#

python
from sklearn.metrics import (roc_auc_score, average_precision_score,
precision_score, recall_score, f1_score)
 
x = df['passengers'].values
y_true = df['is_anomaly'].values
 
detectors = {
'Z-Score (k=3)': lambda x: detector_zscore(x, 3.0),
'Modified Z (k=3.5)': lambda x: detector_modified_z(x, 3.5),
'IQR (k=1.5)': lambda x: detector_iqr(x, 1.5),
'Adjusted Boxplot': lambda x: detector_adjusted_boxplot(x, 1.5),
'POT (q=0.001)': lambda x: detector_pot(x, 95, 0.001),
}
 
results = []
for name, fn in detectors.items():
scores, preds = fn(x)
try:
auc = roc_auc_score(y_true, scores)
ap = average_precision_score(y_true, scores)
except ValueError:
auc, ap = np.nan, np.nan
if preds.sum() > 0:
prec = precision_score(y_true, preds, zero_division=0)
rec = recall_score(y_true, preds, zero_division=0)
f1 = f1_score(y_true, preds, zero_division=0)
else:
prec, rec, f1 = 0, 0, 0
results.append({
'Detector': name,
'ROC-AUC': auc,
'PR-AUC': ap,
'Precision': prec,
'Recall': rec,
'F1': f1,
'Alarm rate': preds.mean(),
})
 
results_df = pd.DataFrame(results)
print(results_df.round(3).to_string(index=False))
5 detektörü koştur ve metrik topla

Adım 5: Sonuçları Yorumla#

Tipik bir benchmark çıktısı şuna benzer (gerçek değerler veriyle değişir):
Detector ROC-AUC PR-AUC Precision Recall F1 Alarm rate Z-Score (k=3) 0.612 0.082 0.105 0.398 0.166 0.092 Modified Z (k=3.5) 0.671 0.115 0.156 0.464 0.234 0.073 IQR (k=1.5) 0.654 0.098 0.121 0.421 0.188 0.085 Adjusted Boxplot 0.689 0.142 0.184 0.502 0.270 0.067 POT (q=0.001) 0.732 0.179 0.243 0.521 0.331 0.052

Önemli Gözlemler#

1. POT en güçlü. Çünkü NYC Taxi verisi heavy-tail: extreme yüksek talep (yılbaşı) ve extreme düşük talep (blizzard) günler var. POT bunu modelinde doğrudan yakalıyor.
2. Adjusted boxplot ikinci. Veri right-skewed (zirve saatlerdeki yüksek talep değerleri), klasik IQR'a göre %15-20 daha iyi.
3. Klasik z-score en zayıf. Çünkü:
  • Veri normal dağılım göstermiyor
  • Mean ve std outlier'lardan etkileniyor
  • Tek statik eşik (3σ) zaman içindeki bağlamı yok sayıyor
4. Hiçbiri %80+ ROC-AUC vermiyor. Çünkü bu yöntemler bağlamsız. NYC Maratonu günü çok yolcu olur — bu istatistiksel olarak "normalin uçunda" ama olağan. Bunu yakalamak için bağlamsal yöntemlere (Modül 15-16 — Prophet, LSTM-AE) gerek var.

Adım 6: Her Detektörün Tespitini Görselleştir#

python
fig, axes = plt.subplots(len(detectors), 1, figsize=(16, 14), sharex=True)
 
for ax, (name, fn) in zip(axes, detectors.items()):
scores, preds = fn(x)
ax.plot(df['timestamp'], df['passengers'],
color='steelblue', linewidth=0.3, alpha=0.6)
 
# Tespit edilen anomaly'ler
detected_mask = preds & ~y_true.astype(bool) # tespit ama gerçek değil = FP
correct_mask = preds & y_true.astype(bool) # tespit + gerçek = TP
 
ax.scatter(df.loc[detected_mask, 'timestamp'],
df.loc[detected_mask, 'passengers'],
c='orange', s=10, alpha=0.5, label='FP (tespit ama gerçek değil)')
ax.scatter(df.loc[correct_mask, 'timestamp'],
df.loc[correct_mask, 'passengers'],
c='red', s=20, alpha=0.8, label='TP (doğru tespit)')
 
# Gerçek anomaly window'ları gölgele
for start, end in nyc_windows:
ax.axvspan(start, end, color='crimson', alpha=0.08)
 
ax.set_title(name, fontsize=11)
ax.legend(loc='upper left', fontsize=8)
ax.set_ylabel('Yolcu')
 
plt.tight_layout()
plt.savefig('reports/nyc_taxi_detectors.png', dpi=120)
plt.show()
Her detektörün tespitini görselleştir

Adım 7: Bonus — Rolling Window ile Bağlam Ekle#

Bu detektörlerin temel sorunu statik olmaları. Gerçek production'da yöntemlerin rolling window üzerinde uygulanması performansı artırır. Hızlı bir 24 saatlik rolling z-score deneyelim:
python
def rolling_zscore(x, window=24):
"""24 saatlik rolling z-score (saatlik veri için günlük bağlam)."""
s = pd.Series(x)
mu = s.rolling(window, min_periods=window).mean()
sigma = s.rolling(window, min_periods=window).std()
return ((s - mu) / (sigma + 1e-9)).abs().fillna(0).values
 
scores_rolling = rolling_zscore(x, window=24)
preds_rolling = scores_rolling > 3.0
 
auc = roc_auc_score(y_true, scores_rolling)
ap = average_precision_score(y_true, scores_rolling)
prec = precision_score(y_true, preds_rolling, zero_division=0)
rec = recall_score(y_true, preds_rolling, zero_division=0)
f1 = f1_score(y_true, preds_rolling, zero_division=0)
 
print(f"Rolling Z-Score (24h)")
print(f" ROC-AUC: {auc:.3f}")
print(f" PR-AUC: {ap:.3f}")
print(f" P: {prec:.3f}, R: {rec:.3f}, F1: {f1:.3f}")
print(f" Alarm rate: {preds_rolling.mean():.3f}")
Rolling z-score — bağlamlı baseline
📈 Rolling'in faydası
Rolling z-score klasik z-score'a göre tipik olarak %15-30 PR-AUC iyileştirir. Çünkü her saat kendi bağlamında (geçen 24 saate göre) değerlendirilir. Bu, basit ama güçlü bir teknik. Modül 15-16'da daha sofistike bağlamsal modeller (Prophet, LSTM-AE) ile bu yaklaşımı genişleteceğiz.

Adım 8: Ev Ödevi (Açık Uçlu)#

Bu lab'ı bitirdiysen, aşağıdaki açık uçlu çalışmaları dene:

Ödev 1: log-transform#

Yolcu sayısına
np.log1p
uygula, sonra z-score yeniden koştur. PR-AUC nasıl değişir? Neden?

Ödev 2: Birden çok windowlu rolling#

24h, 168h (1 hafta), 720h (1 ay) — üç farklı rolling window'u ensemble yap. Her birinin sonucunu ortalama. Tek window'a göre PR-AUC ne durumda?

Ödev 3: POT eşik tuning#

POT'un
threshold_percentile
parametresini 80, 90, 95, 98, 99 değerleriyle koş. Mean excess plot'la birlikte değerlendir. Optimal değer ne?

Ödev 4: Yarısına göz at, yarısına test et#

Veriyi train/test olarak %50-%50 böl (zaman bazlı). Train üzerinde istatistikleri öğren (mean, std, GPD parametreleri), test üzerinde uygula. Performans nasıl değişir?

Ödev 5: Threshold optimization#

Her detektör için F1'i maksimize eden eşiği grid search ile bul (örn. z-score için 2.0, 2.5, 3.0, 3.5, 4.0). En iyi konfigürasyonlarla yeni karşılaştırma yap.

Çıktıları paylaş#

Bu ödevleri bitirdiysen GitHub repo'na pushle. Topluluk içinde paylaşırsan rozetlere katkı sağlar (kursta gamification var).

Modül 2 Özeti#

Modül 2'yi bitirdin. Elinde şu var:
Ders 2.1 — Z-score, modified z, MAD; klasik vs robust ayrımı
Ders 2.2 — IQR, Tukey's fences, adjusted boxplot, medcouple — skewed veride doğru araç
Ders 2.3 — Grubbs, Dixon, Generalized ESD — hipotez tabanlı testler ve compliance
Ders 2.4 — Chebyshev, EVT, POT — heavy-tail / uç olay modelleme; SPOT/DSPOT
Ders 2.5 — Huber loss, M-estimator, Tukey biweight, MCD — modern AD'nin gizli omurgası
Ders 2.6 — NYC Taxi gerçek verisinde 5 detektörü yan yana benchmark, rolling window iyileştirmesi
Bu modülün matematiği sonraki tüm modüllerin altında yatıyor. Modül 3'te Veri Hazırlığı'na, sonra Modül 4'te Değerlendirme Metrikleri'ne (PR-AUC vs ROC-AUC, NAB scoring, alert fatigue ekonomisi) geçeceğiz.
👉 Bir sonraki modül
Modül 3 — Veri Hazırlığı, Imbalanced Data ve Etiketleme Pratikleri. Class imbalance problemini ve SMOTE/ADASYN ailesini, cost-sensitive learning'i, weak supervision'ı işleyeceğiz. IEEE-CIS Fraud verisinde 4 sampling stratejisinin PR-AUC karşılaştırmasıyla bitireceğiz. /learn/anomali-tespiti sayfasını sık ziyaret et.

Sık Sorulan Sorular

Numenta NAB benchmark'ı endüstri standardıdır. Ama NYC Taxi univariate tek seri — gerçek production senaryolarındaki multivariate (banking ile feature'lar arası ilişki) durumunu temsil etmez. Bu lab'da öğrendiklerin başlangıç noktası; Modül 16-17'de multivariate'a, Modül 19'da gerçek fraud verisine geçiyoruz.

Yorumlar & Soru-Cevap

(0)
Yorum yazmak için giriş yap.
Yorumlar yükleniyor...

İlgili İçerikler