Ana içeriğe geç
Kesit Python
İleri · Makine öğrenmesi

Zaman serisi tahmini: temel çizgiden ETS, ARIMA ve ML'ye

Ufuk, sıklık ve toplama seviyesi; naive ve mevsimsel naive temel çizgileri; STL ayrıştırma ve ADF; Holt-Winters (ETS) ve dışsal değişkenli SARIMAX; lag özellikli ML, özyinelemeli ve doğrudan çok adımlı tahmin; genişleyen pencere backtest; MAE, RMSE, sMAPE, MASE ve tahmin aralığı. Pedal İzmir saatlik talep + hava verisiyle.

Uygulamada aç, 20 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Yarın kaç bisiklet gerekecek?

Pedal İzmir operasyon ekibi her gece kamyonlarla bisikletleri istasyonlar arasında taşıyor. Sabah 8'de Alsancak'ta bisiklet kalmazsa müşteri kaybediliyor, Bostanlı'da yığılırsa boş yuva kalmıyor. Bunun için yarının saatlik talebini bilmeleri gerekiyor. Bu bir zaman serisi tahmini (time series forecasting) problemidir: geçmiş gözlemlerin sırasından ve düzeninden geleceği çıkarmak.

Zaman serisinin pandas tarafını (resample, shift, rolling) Zaman serisi analizi dersinde gördün. Bu derste tahmin yapacaksın: önce yenmesi gereken temel çizgiler, sonra bileşen ayrıştırma, üstel düzeltme (ETS), ARIMA/SARIMAX, lag özellikli makine öğrenmesi ve hepsini dürüstçe karşılaştıran geriye dönük test (backtest). Veri: Pedal İzmir'in Haziran–Ağustos 2024 yolculukları (116 517 yolculuk) ve saatlik hava gözlemleri.

Tahmin problemini tanımla: ufuk, sıklık, toplama seviyesi

KararSoruPedal İzmir örneği
Ufuk (horizon)Ne kadar ileriyi tahmin ediyorsun?Yarının 24 saati (ufuk 1–24 saat ya da gün öncesi)
SıklıkHangi zaman adımında?Saatlik (operasyon), günlük (bütçe)
Toplama seviyesiHangi birimde?Şehir toplamı mı, istasyon başına mı?
Bilgi anıTahmin anında ne biliniyor?Dünün talebi ve yarının hava tahmini; bugünün öğleden sonrası değil

Toplama seviyesi yükseldikçe seri düzgünleşir ve tahmin kolaylaşır: tek bir istasyonun saatlik talebi gürültülüdür, şehrin toplamı değildir. "Bilgi anı" ise sızıntının kaynağıdır: yarını tahmin ederken lag 1 (bir saat önce) kullanamazsın; en kısa lag 24 saattir.

import numpy as np
import pandas as pd
import veri

yol = veri.oku("pedal_izmir", "bk_trips", usecols=["started_at"], parse_dates=["started_at"])
saatlik = yol.set_index("started_at").resample("h").size()
saatlik = saatlik.reindex(pd.date_range("2024-06-01", "2024-08-31 23:00", freq="h"),
                          fill_value=0).rename("talep")       # yolculuksuz saat = 0
gunluk = saatlik.resample("D").sum()
print(len(saatlik), "saat,", len(gunluk), "gün; toplam", saatlik.sum())
print("Saat profili:", saatlik.groupby(saatlik.index.hour).mean().round(0).astype(int).tolist())
print("Gün profili:", gunluk.groupby(gunluk.index.dayofweek).mean().round(0).astype(int).tolist())

İki mevsimsellik var: günlük (sabah 8 ve akşam 18 tepeleri, gece neredeyse sıfır) ve haftalık (hafta sonu daha düşük). Saatlik seride haftalık döngü 168 adımdır.

Temel çizgiler: önce bunları yen

Karmaşık bir model kurmadan önce basit tahmincilerin skorunu bil. Şaşırtıcı sıklıkla en iyileri bunlardır.

egitim, test = saatlik[:-168], saatlik[-168:]          # son hafta test
tahminler = {
    "naive": np.repeat(egitim.iloc[-1], 168),
    "mevsimsel_24": np.tile(egitim[-24:].to_numpy(), 7),
    "mevsimsel_168": egitim[-168:].to_numpy(),
    "ort_168": np.repeat(egitim[-168:].mean(), 168),
}
for ad, t in tahminler.items():
    print(f"{ad:14s} MAE = {np.mean(np.abs(test.to_numpy() - t)):.2f}")

Geçen haftanın aynı saati (MAE ≈ 7.7) dünün aynı saatinden (≈ 26) çok daha iyi; çünkü hafta sonu ile hafta içini ayırıyor. Bu, yenmen gereken çıtadır.

Metrikler ve tuzakları

MetrikFormül (özet)Tuzak
MAEortalama |y − ŷ|Ölçeğe bağlı; seriler arası kıyas yapılamaz
RMSE√ortalama (y − ŷ)²Büyük hataları ağır cezalandırır (yağmurlu saatler)
MAPEortalama |y − ŷ| / |y|y = 0 olduğunda tanımsız; gece saatleri sonsuz hata üretir; düşük tahmini kayırır
sMAPEortalama 2|y − ŷ| / (|y| + |ŷ|)0–200 arası; ikisi de 0 ise 0 say
MASEMAE / eğitimdeki mevsimsel naive MAE'si1'in altı = mevsimsel naive'den iyi; ölçekten bağımsız
MAPE ve sıfırlarPedal İzmir serisinde 173 saat sıfır talep var. MAPE bu saatlerde sıfıra böler. Operasyon ekibine "yüzde hata" vermen gerekiyorsa toplam üzerinden WAPE (Σ|y − ŷ| / Σ|y|) kullan.

Bileşen ayrıştırma ve durağanlık

STL (Seasonal-Trend decomposition using Loess) seriyi üç parçaya ayırır: trend, mevsimsel bileşen ve kalıntı. Kalıntının varyansı mevsimsel+kalıntıya göre küçükse mevsimsellik güçlüdür: F = max(0, 1 − Var(kalıntı) / Var(mevsim + kalıntı)). ARIMA ailesi ise durağan (stationary: ortalaması ve varyansı zamanla değişmeyen) seri ister. ADF testi (Augmented Dickey-Fuller) "seride birim kök var, durağan değil" boş hipotezini sınar; p < 0.05 ise durağan kabul edilir. Durağan değilse fark alınır (ARIMA'daki d).

from statsmodels.tsa.seasonal import STL
from statsmodels.tsa.stattools import adfuller

stl = STL(saatlik, period=24).fit()
guc = max(0.0, 1 - np.var(stl.resid) / np.var(stl.seasonal + stl.resid))
print("Günlük mevsimsellik gücü:", round(guc, 3))
print("ADF p (günlük toplam):", f"{adfuller(gunluk)[1]:.2e}")

Üstel düzeltme (ETS, Holt-Winters)

Üstel düzeltme yakın geçmişe daha çok ağırlık verir. Holt-Winters bunu üç bileşene uygular: seviye, trend ve mevsim. statsmodels'ta ExponentialSmoothing sınıfıyla kurulur; seasonal="add" mevsimsel etkinin sabit büyüklükte, "mul" seviyeyle orantılı olduğunu varsayar.

import warnings
from statsmodels.tsa.holtwinters import ExponentialSmoothing
warnings.filterwarnings("ignore")

g_egt, g_test = gunluk[:-14], gunluk[-14:]
ets = ExponentialSmoothing(g_egt, seasonal="add", seasonal_periods=7).fit()
ets_t = ets.forecast(14)
mevs = np.tile(g_egt[-7:].to_numpy(), 2)
print("ETS MAE:", round(np.mean(np.abs(g_test - ets_t)), 1),
      " mevsimsel naive MAE:", round(np.mean(np.abs(g_test.to_numpy() - mevs)), 1))

ARIMA ve SARIMAX: dışsal değişkenle tahmin

ARIMA(p, d, q) seriyi kendi geçmiş değerleri (AR, p), fark alma (I, d) ve geçmiş hataları (MA, q) ile modeller. SARIMA buna mevsimsel bir parça ekler: seasonal_order=(P, D, Q, s). SARIMAX'teki X, dışsal (exogenous) değişkendir: yağış gibi talebi etkileyen ama talepten etkilenmeyen bir seri. Günlük talep ile günlük yağış arasındaki korelasyon −0.90; yağmurlu bir günü yalnız geçmişe bakan bir model göremez.

from statsmodels.tsa.statespace.sarimax import SARIMAX

hava = veri.oku("pedal_izmir", "bk_weather", parse_dates=["obs_hour"]).set_index("obs_hour")
yagis = hava["rain_mm"].resample("D").sum().rename("yagis").to_frame()
sx = SARIMAX(g_egt, exog=yagis[:-14], order=(1, 0, 0), seasonal_order=(1, 0, 0, 7),
             trend="c").fit(disp=False, maxiter=200)
ongoru = sx.get_forecast(14, exog=yagis[-14:])         # gelecek yağış = hava tahmini
print("yağış katsayısı:", round(sx.params["yagis"], 1))
print("SARIMAX MAE:", round(np.mean(np.abs(g_test - ongoru.predicted_mean)), 1))
print(ongoru.conf_int(alpha=0.2).round(0).head(3))    # %80 tahmin aralığı
Dışsal değişkenin bedeliTest döneminde gerçek yağışı kullandık; canlıda elinde yalnız hava tahmini olacak ve o da hatalıdır. Dürüst bir backtest, o gün bilinen hava tahminiyle yapılır. Bilinmeyen dışsal değişken (ör. yarının gerçek sıcaklığı) sızıntıdır.

Lag özellikli makine öğrenmesi

Zaman serisini tablo problemine çevirebilirsin: her satır bir saat; özellikler saat, hafta sonu, lag 24, lag 168, hava; hedef o saatin talebi. Sonra Ridge, gradient boosting ya da herhangi bir regresyon modeli. Çok adımlı tahminde iki strateji var:

Lag'leri ufuktan küçük seçmezsen (gün öncesi tahminde lag ≥ 24) iki strateji de gerekmez: tüm özellikler tahmin anında bilinir.

from sklearn.linear_model import Ridge
from sklearn.model_selection import TimeSeriesSplit

d = saatlik.to_frame().join(hava[["rain_mm", "temp_c"]])
d["lag24"], d["lag168"] = d["talep"].shift(24), d["talep"].shift(168)
d["hs"] = (d.index.dayofweek >= 5).astype(int)
d = d.dropna()
X = pd.get_dummies(d.index.hour, prefix="s").astype(float).set_index(d.index)
X = X.join(d[["hs", "lag24", "lag168", "rain_mm", "temp_c"]])
for i, (egt, tst) in enumerate(TimeSeriesSplit(n_splits=3, test_size=168).split(X)):
    m = Ridge().fit(X.iloc[egt], d["talep"].iloc[egt])
    mae = np.mean(np.abs(m.predict(X.iloc[tst]) - d["talep"].iloc[tst]))
    print(f"kat {i}: eğitim {len(egt)} saat, test {X.index[tst[0]].date()} → MAE {mae:.2f}")

Geriye dönük test: genişleyen pencere

Zaman serisinde rastgele bölme, modelin geleceği görerek geçmişi tahmin etmesi demektir. Doğrusu genişleyen pencere (expanding window): 1 Ağustos'a kadar eğit, bir hafta tahmin et; 8 Ağustos'a kadar eğit, bir sonraki haftayı tahmin et... TimeSeriesSplit bunu otomatik yapar; test_size ufku, gap eğitim ile test arasındaki boşluğu (bilgi gecikmesi) ayarlar. Tek bir test haftası şanslı ya da şanssız olabilir; birkaç katın ortalaması ve dağılımı karar için gerekir.

Tahmin aralığıOperasyon ekibi tek sayı değil aralık ister: "yarın 8–9 arası Alsancak'ta 18 ile 30 arası kiralama". Model tabanlı aralıklar (SARIMAX conf_int) varsayımlara dayanır. Daha sağlam yol: backtest hatalarının yüzdelikleri. Geçmiş hataların %10 ve %90 yüzdeliklerini tahmine ekle, sonra yeni dönemde gerçeklerin yüzde kaçının aralığa düştüğünü (kapsama) ölç.
Gerçek işteBisiklet paylaşımı, yemek teslimatı ve perakende şirketlerinde talep tahmini genellikle iki katmanlıdır: şehir/gün düzeyinde istatistiksel model (ETS, SARIMAX) bütçe ve vardiya planı için; istasyon/saat düzeyinde lag ve hava özellikli gradient boosting operasyon için. Her sabah backtest panosu güncellenir ve model mevsimsel naive'i yenmediği hafta alarm üretir.
Uzmanlaş

Hiyerarşik tahmin

İstasyon tahminlerinin toplamı şehir tahminine eşit olmalı (tutarlılık, coherence). Alttan yukarı topla, üstten aşağı oranla dağıt ya da MinT uzlaştırma kullan (Nixtla hierarchicalforecast).

Kütüphane manzarası

statsmodels klasik modeller; sktime ve Darts tek arayüzde istatistiksel + ML modelleri ve backtest; Nixtla statsforecast binlerce seriyi hızlı ETS/ARIMA ile; Prophet tatil ve çoklu mevsimsellikte pratik; LightGBM + lag M5 yarışmasının kazananı. Derin öğrenme (N-BEATS, TFT) çok sayıda ilişkili seride öne çıkar.

Mülakat cümlesi

"Önce mevsimsel naive temel çizgisini ve MASE'i raporlarım. Bölmeyi zamana göre, genişleyen pencereyle yaparım; özellikleri tahmin anında bilinenlerle sınırlarım. MAPE'yi sıfırlı serilerde kullanmam; aralıkları backtest hatalarından kurar ve kapsamasını ölçerim."

Alıştırmalara geç (20)