Ana içeriğe geç
Kesit Python
İleri · Veri analizi ve bilimi

Zaman serisi analizi

to_datetime, DatetimeIndex ve dt erişimcisi, resample ile kovalama, shift/diff/pct_change ile dönem kıyası, rolling("7D"), eksik günleri doldurma, saat dilimi, iş günü ve mevsimsellik; Anadolu Market ve Ilgaz Bank verisiyle.

Uygulamada aç, 35 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ç.

Zaman, verinin en önemli sütunu

Anadolu Market'in yönetimi "bu ay geçen aydan iyi mi?", "kampanya haftasında satış gerçekten arttı mı, yoksa her ayın ilk haftası zaten böyle mi?" diye sorar. Ilgaz Bank'ın operasyon ekibi ATM işlemlerinin saatlik yoğunluğuna, risk ekibi gece yarısı yapılan olağandışı işlemlere bakar. Bu soruların hepsinde satırların bir zaman damgası vardır ve cevap, zamanı doğru işlemekten geçer.

groupby dersinde resample'a kısaca, ileri pandas dersinde rolling'e değindin. Bu ders zaman serisi (time series) analizinin tamamını kurar: metni tarihe çevirmek, tarih index'iyle dilimlemek, kovalara toplamak, önceki dönemle kıyaslamak, eksik günleri doldurmak, saat dilimi, iş günü ve mevsimsellik.

Metni tarihe çevirmek: to_datetime

CSV'den gelen tarih her zaman metindir. pd.to_datetime onu datetime64 türüne çevirir. Biçimi (format) açıkça ver: hem hızlıdır hem de "03.05.2024 üç mayıs mı, beş mart mı?" belirsizliğini ortadan kaldırır. Bozuk değerler için errors="coerce" hata yerine NaT (Not a Time, tarihin NaN'ı) üretir.

import pandas as pd

ham = pd.DataFrame({
    "tarih": ["05.03.2024", "17.03.2024", "31.02.2024", "02.04.2024", "bilinmiyor"],
    "tutar": [120.0, 340.5, 99.0, 410.0, 55.0],
})
ham["tarih"] = pd.to_datetime(ham["tarih"], format="%d.%m.%Y", errors="coerce")
print(ham)
print("bozuk tarih sayısı:", ham["tarih"].isna().sum())
temiz = ham.dropna(subset=["tarih"])

# Unix zaman damgası (saniye) de sık gelir: bankacılık API'leri böyle verir
print(pd.to_datetime([1709251200, 1709337600], unit="s"))

31 Şubat diye bir gün yok; coerce onu sessizce NaT yaptı. Bozuk satır sayısını mutlaka raporla: on satırsa yazım hatasıdır, binlerce satırsa biçimi yanlış seçmişsindir.

DatetimeIndex: tarihle dilimleme

Tarih sütununu index yapınca (set_index("tarih")) pandas zamanı anlayan bir index kurar: DatetimeIndex. Artık "Mart ayı" ya da "5-10 Mart arası" gibi kısmi metinlerle dilimleyebilirsin. Önce sort_index() çağır; sırasız tarih index'inde dilimleme güvenilmezdir.

import pandas as pd

gunler = pd.date_range("2024-02-26", "2024-03-12", freq="D")
ciro = pd.Series(range(100, 100 + 10 * len(gunler), 10), index=gunler, name="ciro")

print(ciro.loc["2024-03"].head(3))               # Mart'ın ilk üç günü
print(ciro.loc["2024-03-05":"2024-03-07"])       # iki uç dahil
print(ciro.index.min().date(), "→", ciro.index.max().date(), len(ciro), "gün")

dt erişimcisi: tarihin parçaları

Tarih sütun olarak duruyorsa parçalarına .dt ile, index olarak duruyorsa doğrudan index.month gibi erişirsin.

İfadeSonuç (2024-03-04 Pazartesi)Kullanım
.dt.year, .dt.month, .dt.day2024, 3, 4Yıl/ay kırılımı
.dt.dayofweek0 (Pazartesi=0, Pazar=6)Hafta içi/sonu analizi
.dt.quarter1Çeyrek raporu
.dt.hour0Saatlik yoğunluk
.dt.to_period("M")2024-03Ay anahtarı (raporda gruplama)
.dt.normalize()2024-03-04 00:00Saati at, günü tut
.dt.is_month_endFalseAy sonu kapanışları
import pandas as pd

islem = pd.DataFrame({
    "zaman": pd.to_datetime(["2024-03-04 09:15", "2024-03-09 23:40", "2024-03-10 14:05", "2024-03-29 18:30"]),
    "tutar": [1500, 250, 980, 4300],
})
islem["gun"] = islem["zaman"].dt.dayofweek
islem["hafta_sonu"] = islem["gun"] >= 5
islem["saat"] = islem["zaman"].dt.hour
islem["ay"] = islem["zaman"].dt.to_period("M").astype(str)
print(islem)
print(islem.groupby("hafta_sonu")["tutar"].sum())
Gün adı için sözlük kullandt.day_name() İngilizce ad döndürür ve Türkçe yerel ayarı her ortamda bulunmaz. Raporda Türkçe ad istiyorsan dayofweek ile bir listeden eşle: ["Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"].

resample: zamanı kovalara bölmek

resample bir DatetimeIndex'i düzenli aralıklara böler, her aralığa bir özet uygular: günlük işlemlerden haftalık ciro, aylık ortalama, çeyreklik toplam. groupby'ın zamana özel hâlidir; fark, veride hiç kaydı olmayan aralıkları da üretmesidir (boş ay 0 ya da NaN olarak görünür).

Takma adAnlamıEtiketNot
"D"GünO günHer sürümde aynı
"W"Hafta (Pazartesi–Pazar)Pazar günü"W-MON" gibi çapa seçilebilir
"MS"AyAyın ilk günüRapor için en okunur
"ME"AyAyın son günüpandas 2.2+; eski "M" kaldırılıyor
"QS" / "QE"ÇeyrekBaşı / sonuEski "Q" yerine
"h"SaatSaat başıBüyük "H" kaldırılıyor
import pandas as pd

siparis = pd.DataFrame({
    "tarih": pd.to_datetime(["2024-01-05", "2024-01-20", "2024-03-02", "2024-03-15", "2024-03-28"]),
    "tutar": [100.0, 200.0, 50.0, 80.0, 120.0],
}).set_index("tarih")

aylik = siparis["tutar"].resample("MS").sum()
print(aylik)                                  # Şubat 0 olarak geldi
print(siparis["tutar"].resample("MS").agg(["sum", "count", "mean"]))
print(siparis.resample("QS")["tutar"].sum())

# DatetimeIndex yapmadan: on= parametresi
print(siparis.reset_index().resample("MS", on="tarih")["tutar"].sum().tolist())
"M" yazmapandas 2.2 "M", "Q", "H" için uyarı verir, pandas 3 bunları reddeder. Uygulamada tarayıcı pandas 2.2, senin bilgisayarın 3.x olabilir; iki sürümde de çalışan "MS", "ME", "QS", "QE", "h" kullan ya da dt.to_period("M") ile grupla.

shift, diff, pct_change: önceki dönemle kıyas

shift(1) seriyi bir adım aşağı kaydırır: her satır "bir önceki dönemin değerini" görür (SQL'deki LAG). diff() farkı, pct_change() oransal değişimi verir. Aylık seride shift(12) "geçen yılın aynı ayı"dır: yıldan yıla (year over year, YoY) büyüme böyle hesaplanır ve mevsimselliği kendiliğinden ayıklar; Aralık'ı Kasım'la değil, geçen Aralık'la kıyaslarsın.

import pandas as pd

aylar = pd.date_range("2023-01-01", periods=15, freq="MS")
ciro = pd.Series([100, 95, 110, 120, 118, 130, 125, 128, 135, 140, 160, 210,
                  112, 104, 121], index=aylar, name="ciro")
tablo = pd.DataFrame({
    "ciro": ciro,
    "onceki_ay": ciro.shift(1),
    "fark": ciro.diff(),
    "aylik_%": (ciro.pct_change() * 100).round(1),
    "yoy_%": ((ciro / ciro.shift(12) - 1) * 100).round(1),
})
print(tablo.tail(4))

Ocak 2024'te aylık değişim -46,7% görünüyor; panik yok, Aralık her yıl zirvedir. YoY sütunu gerçeği söylüyor: geçen Ocak'a göre +12%. Yönetime giden raporda iki ölçüyü birlikte vermek yanlış alarmı önler.

Hareketli ortalama: satır mı, zaman mı?

rolling(7) son 7 satırı, rolling("7D") son 7 günü alır. Her gün kaydın varsa ikisi aynıdır. Ama mağaza pazar günleri kapalıysa ya da veri bazı günlerde eksikse rolling(7) fark etmeden 9-10 güne yayılır. Zaman tabanlı pencere DatetimeIndex ister ve pencere dolmasa da değer üretir.

import pandas as pd

s = pd.Series([10, 12, 30, 11, 13],
              index=pd.to_datetime(["2024-05-01", "2024-05-02", "2024-05-06", "2024-05-07", "2024-05-08"]))
print(pd.DataFrame({
    "satis": s,
    "son3_satir": s.rolling(3, min_periods=1).mean().round(2),
    "son3_gun": s.rolling("3D").mean().round(2),
}))

Eksik tarihleri doldurmak

Satış olmayan gün tabloda hiç yoktur; ama "satış 0" ile "veri yok" farklı şeylerdir ve grafikte, ortalamada, shift'te fark yaratır. Tam takvimi pd.date_range ile kurup reindex et ya da asfreq("D") kullan.

import pandas as pd

satis = pd.Series([5, 7, 3], index=pd.to_datetime(["2024-05-01", "2024-05-03", "2024-05-06"]))
takvim = pd.date_range(satis.index.min(), satis.index.max(), freq="D")
print(satis.reindex(takvim, fill_value=0).tolist())     # satış: yoksa 0

stok = pd.Series([40, 32, 28], index=satis.index)
print(stok.asfreq("D").ffill().tolist())               # stok seviyesi: son bilinen değer
Ölçü türüEksik gün nasıl doldurulur?Örnek
Akış (flow)0Satış adedi, işlem sayısı, ciro
Durum (stock)Son bilinen değer (ffill)Stok, hesap bakiyesi, döviz kuru
ÖlçümAra değer (interpolate) ya da boş bırakSıcaklık, sensör verisi

Saat dilimi: tz_localize ve tz_convert

Sunucular genellikle UTC saklar; rapor ise İstanbul saatiyle istenir. Saat dilimi bilgisi olmayan (naive) bir zamana önce tz_localize("UTC") ile "bu UTC'dir" dersin, sonra tz_convert("Europe/Istanbul") ile çevirirsin. Türkiye 2016'dan beri yıl boyu UTC+3'tür; yaz saati uygulayan bölgelerde (Europe/Berlin gibi) dönüşümü elle +3 eklemek yerine mutlaka bu fonksiyonlarla yap.

import pandas as pd

kayit = pd.DataFrame({"zaman_utc": pd.to_datetime(["2024-03-01 21:30", "2024-03-02 06:10", "2024-07-15 12:00"])})
kayit["istanbul"] = kayit["zaman_utc"].dt.tz_localize("UTC").dt.tz_convert("Europe/Istanbul")
kayit["berlin"] = kayit["istanbul"].dt.tz_convert("Europe/Berlin")
kayit["ist_saat"] = kayit["istanbul"].dt.hour
print(kayit.to_string())
print("İstanbul'da gün değişti mi:", (kayit["istanbul"].dt.date != kayit["zaman_utc"].dt.date).tolist())
Naive ile aware karışmazSaat dilimli bir zamanı dilimsiz bir zamanla karşılaştırmak TypeError verir. Bir tabloda tek bir kural seç: ya hepsi UTC-aware ya hepsi yerel-naive. Gece yarısına yakın işlemlerde yanlış dilim, işlemi bir önceki güne yazar ve günlük mutabakatı bozar.

İş günü

"Kredi başvurusu 5 iş günü içinde sonuçlanır" kuralı hafta sonlarını ve resmî tatilleri saymaz. pd.bdate_range Pazartesi–Cuma günlerini üretir; pd.offsets.BDay(n) tarihe n iş günü ekler; np.busday_count iki tarih arasındaki iş gününü (bitiş hariç) tatil listesiyle sayar. Resmî tatil takvimini sen verirsin; kütüphane Türkiye tatillerini bilmez.

import numpy as np
import pandas as pd

basvuru = pd.Timestamp("2024-04-19")          # Cuma
print("5 iş günü sonra:", (basvuru + pd.offsets.BDay(5)).date())
nisan = pd.bdate_range("2024-04-01", "2024-04-30")
print("Nisan iş günü:", len(nisan))
tatiller = ["2024-04-10", "2024-04-11", "2024-04-12", "2024-04-23"]   # bayram ve 23 Nisan
print("tatiller hariç:", np.busday_count("2024-04-01", "2024-05-01", holidays=tatiller))

Mevsimsellik: haftanın günü etkisi

Perakende satışı haftanın gününe göre düzenli dalgalanır: hafta sonu yüksek, salı düşük. Bu mevsimsellik (seasonality) bilinmeden "salı satışı düştü" alarmı anlamsızdır. En basit ölçü gün endeksidir: her günün ortalaması / genel ortalama. Daha sistematik yol, seriyi trend + mevsimsel + artık (residual) bileşenlerine ayıran seasonal_decompose'dur (statsmodels).

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

rng = np.random.default_rng(42)
gun = pd.date_range("2024-01-01", periods=56, freq="D")          # 8 tam hafta
hafta_deseni = np.array([90, 80, 85, 95, 110, 140, 130])          # Pzt..Paz
trend = np.linspace(0, 20, len(gun))
satis = pd.Series(np.tile(hafta_deseni, 8) + trend + rng.normal(0, 3, len(gun)), index=gun)

endeks = (satis.groupby(satis.index.dayofweek).mean() / satis.mean()).round(2)
print("gün endeksi:", endeks.tolist())

ayrisim = seasonal_decompose(satis, model="additive", period=7)
print("mevsimsel (ilk hafta):", ayrisim.seasonal.iloc[:7].round(1).tolist())
print("trend başı-sonu:", round(ayrisim.trend.dropna().iloc[0], 1), "→", round(ayrisim.trend.dropna().iloc[-1], 1))

Cumartesi endeksi 1.3 civarında: normal bir cumartesi ortalamanın %30 üstündedir. Kampanya etkisini ölçerken ham satışı değil, bu endekse göre düzeltilmiş satışı karşılaştır.

Gerçek işteBir e-ticaret şirketinin haftalık performans toplantısındaki standart tablo: bu haftanın cirosu, geçen haftaya göre değişim, geçen yılın aynı haftasına göre değişim ve 4 haftalık hareketli ortalama. Bankacılıkta gün sonu mutabakatı UTC kayıtların İstanbul saatine doğru çevrilmesine, SLA (hizmet süresi) raporları iş günü hesabına dayanır. Bu tabloların hepsi bu dersteki on kadar fonksiyonla kurulur; hata çoğunlukla eksik günlerin 0 yerine yok sayılmasından ve saat diliminden gelir.
Derinleş

Derinleş: 117 bin bisiklet yolculuğuyla zaman serisi

Zaman serisi tekniklerinin gücü, veri büyüyünce ortaya çıkar. Pedal İzmir, İzmir'de bir bisiklet paylaşım sistemi: pedal_izmir setinde Haziran–Ağustos 2024 arasındaki 116.517 yolculuk (bk_trips) ve aynı dönemin saatlik hava durumu (bk_weather) var. Bu bölümde olay düzeyindeki (her satır bir yolculuk) veriyi günlük ve saatlik serilere çevirecek, mevsimsellik ve dış etken (yağmur) analizi yapacaksın.

Büyük tabloyu akıllı okumak

Tablonun tüm sütunlarına ihtiyacın yoksa usecols ile yalnız gerekenleri oku ve tarih sütunlarını parse_dates ile doğrudan datetime yap. Tarayıcıda bu, okuma süresini ve belleği belirgin biçimde azaltır. Kapanmamış yolculuklarda ended_at boştur; süre hesabında bunlar NaN olur ve median, mean onları kendiliğinden atlar.

import veri

t = veri.oku("pedal_izmir", "bk_trips", usecols=["started_at", "ended_at", "fare"],
             parse_dates=["started_at", "ended_at"])
print(len(t), t["started_at"].min(), t["started_at"].max())
sure = (t["ended_at"] - t["started_at"]).dt.total_seconds() / 60
print("açık yolculuk:", int(sure.isna().sum()), "| medyan süre (dk):", sure.median())

Olaydan seriye: resample ile sayım

Her satır bir olay olduğunda, set_index("started_at").resample("D").size() günlük yolculuk sayısını verir. size hiç yolculuk olmayan günü de 0 olarak gösterir; groupby(dt.date) ise boş günleri atlar. Bu yüzden takvim tamlığı gereken her hesapta resample tercih edilir.

import veri

t = veri.oku("pedal_izmir", "bk_trips", usecols=["started_at"], parse_dates=["started_at"])
gunluk = t.set_index("started_at").resample("D").size()
print(len(gunluk), "gün, toplam", int(gunluk.sum()))
print("en yoğun:", gunluk.idxmax().date(), int(gunluk.max()))
print("7 günlük hareketli ortalama (son gün):", round(gunluk.rolling("7D").mean().iloc[-1], 1))

GUNLER = ["Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"]
hafta = gunluk.groupby(gunluk.index.dayofweek).mean().round(0)
print(dict(zip(GUNLER, hafta.astype(int).tolist())))
Gün adlarını sen verdt.day_name() İngilizce ad döndürür; locale parametresi işletim sistemine bağlıdır ve tarayıcıda çalışmayabilir. dayofweek (0 = Pazartesi) ile bir Türkçe ad listesi kullanmak her ortamda aynı sonucu verir.

Saatlik profil ve iki seriyi zamanda eşlemek

Hava durumu saatlik ölçülür (obs_hour tam saat). Yolculukları saatlik seriye çevirirsen iki tabloyu aynı zaman anahtarıyla birleştirebilirsin. Sayım serisinde yolculuk olmayan saat 0 olmalıdır; bunu how="left" ve fillna(0) sağlar.

import veri

t = veri.oku("pedal_izmir", "bk_trips", usecols=["started_at"], parse_dates=["started_at"])
w = veri.oku("pedal_izmir", "bk_weather", parse_dates=["obs_hour"])

saatlik = t.set_index("started_at").resample("h").size().rename("yolculuk")
m = w.set_index("obs_hour").join(saatlik, how="left").fillna({"yolculuk": 0})
print(len(m), "saat")
print(m.groupby(m["rain_mm"] > 0)["yolculuk"].mean().round(1).to_dict())
print("yağmurlu saatler:", sorted(m.index[m["rain_mm"] > 0].hour.unique().tolist()))

Gün × saat matrisi: talebin parmak izi

Operasyon ekipleri talebi çoğu zaman bir matris olarak görmek ister: satırlarda haftanın günü, sütunlarda saat, hücrede o gün-saat ikilisinin ortalama yolculuk sayısı. Toplam değil ortalama alınır, çünkü her gün adı dönemde farklı sayıda geçer. Bu matris ısı haritasına (heatmap) dönüştürüldüğünde hafta içi sabah ve akşam iki tepeyi, hafta sonu ise öğleden sonraya yayılan tek bir tepeyi açıkça gösterir.

import veri

t = veri.oku("pedal_izmir", "bk_trips", usecols=["started_at"], parse_dates=["started_at"])
saatlik = t.set_index("started_at").resample("h").size().rename("n").to_frame()
saatlik["gun"] = saatlik.index.dayofweek
saatlik["saat"] = saatlik.index.hour
matris = saatlik.pivot_table(index="gun", columns="saat", values="n", aggfunc="mean").round(0)
print(matris.loc[:, [8, 12, 18, 23]])
Karıştırıcı değişken: saatYukarıdaki ham karşılaştırma "yağmurda yolculuk %60 düşüyor" der. Ama bu veride yağmur yalnızca gündüz saatlerinde yağmış; gündüz zaten yoğun saatlerdir. Adil karşılaştırma için aynı saat dilimindeki yağmurlu ve kuru saatleri kıyaslamalısın. Saat kontrolü yapınca etki çok daha büyük çıkar. Zaman serisinde "neyle kıyaslıyorum?" sorusu, hesaplama tekniğinden daha önemlidir.
Gerçek iştePaylaşımlı ulaşım, yemek teslimatı ve çağrı merkezi gibi işlerde kapasite planı saatlik talep profiline dayanır: kaç bisiklet hangi istasyona, kaç kurye hangi saate, kaç temsilci hangi vardiyaya. Bu profil hafta içi ve hafta sonu ayrı çıkarılır, hava durumu ve tatil gibi dış etkenler ayrı sütun olarak eklenir. Tahmin modelleri (bkz. zaman serisi tahmini dersi) tam olarak bu özellikler üzerine kurulur.
Derinleş

Bu derste zamanı ölçmeyi, kovalara bölmeyi ve dönemleri kıyaslamayı öğrendin. Bir sonraki doğal soru "yarın kaç?" sorusudur, yani tahmin (forecasting). Tahmin modelleri, ARIMA ve üstel düzeltme Zaman serisi tahmini dersinin konusu; bu bölüm o derse köprü kuruyor: geçmişten özellik (feature) üretmek, bunu sızıntısız yapmak ve modeli dürüstçe sınamak. Veri, Pedal İzmir'in Haziran-Ağustos 2024 arasındaki 116 bin bisiklet yolculuğu.

Önce seriyi kur: günlük yolculuk sayısı

import veri
import pandas as pd
yol = veri.oku("pedal_izmir", "bk_trips")
zaman = pd.to_datetime(yol["started_at"])
gunluk = zaman.dt.floor("D").value_counts().sort_index().asfreq("D", fill_value=0)
gunluk.name = "yolculuk"
print(len(gunluk), gunluk.index.min().date(), gunluk.index.max().date())
print(gunluk.head(7).tolist())
print(gunluk.groupby(gunluk.index.dayofweek).mean().round(0).tolist())   # Pzt..Paz

asfreq("D", fill_value=0) hiç yolculuk olmayan günleri de 0 olarak ekler; eksik gün, gecikme (lag) hesabını sessizce kaydırır. Haftanın günü ortalamaları mevsimselliği (seasonality) gösterir: modelin bunu öğrenmesi için ona "geçen haftanın aynı günü" bilgisini vermen gerekir.

Gecikme ve pencere özellikleri

Klasik makine öğrenmesi modelleri (doğrusal regresyon, gradyan artırma) sırayı bilmez; her satırı bağımsız görür. Zaman bilgisini ona sütun olarak verirsin:

ÖzellikpandasAnlamı
lag_1s.shift(1)Dünkü değer
lag_7s.shift(7)Geçen haftanın aynı günü
ort_7s.shift(1).rolling(7).mean()Dünden geriye son 7 günün ortalaması
haftanin_gunus.index.dayofweekTakvimden gelir; gelecek için de bilinir
Sızıntının en sinsi biçimi: shift'siz rollings.rolling(7).mean() bugünün değerini de içerir. Tahmin anında bugünün değeri henüz bilinmediği için model eğitimde "cevabı görür", testte harika, canlıda kötü sonuç verir. Kural: tahmin edilen an t ise, özellik yalnız t'den önce bilinen bilgiyle hesaplanmalı. Pencereyi her zaman shift(1)'den sonra uygula.
X = pd.DataFrame({
    "y": gunluk,
    "lag_1": gunluk.shift(1),
    "lag_7": gunluk.shift(7),
    "ort_7": gunluk.shift(1).rolling(7).mean(),
    "sizan_ort_7": gunluk.rolling(7).mean(),          # YANLIŞ: bugünü içerir
    "haftanin_gunu": gunluk.index.dayofweek,
}).dropna()
print(len(X))
print(X[["y", "ort_7", "sizan_ort_7"]].corr().round(3).loc["y"].tolist())

Sızan özelliğin hedefle korelasyonu belirgin biçimde yüksek çıkar; bu fark "çok iyi" görünen ama canlıda çöken modelin işaretidir. Çok adımlı tahminde (7 gün sonrası) kural daha da sıkıdır: ufuk h ise en yakın gecikme shift(h) olabilir.

Dış değişkenlerde (exogenous) aynı soruyu sor: "Tahmin anında bunu gerçekten bilir miyim?" Pedal İzmir'in bk_weather tablosundaki gerçekleşen yağmur, yarının yolculuğunu tahmin ederken bilinmez; canlıda elinde yalnız hava tahmini olur. Eğitimde gerçekleşen değeri kullanmak modeli olduğundan iyi gösterir. Takvim bilgileri (tatil, okul dönemi, kampanya planı) ise önceden bilinir ve güvenle kullanılır.

Dürüst sınama: zamana göre bölme

Rastgele train_test_split zaman serisinde yasaktır: test günleri eğitim günlerinin arasına serpiştirilir ve model geleceği görür. Doğrusu son N günü test olarak ayırmak ya da ileri yürüyen doğrulama (walk-forward) yapmaktır: önce 1-60. günlerle eğit, 61-67'yi tahmin et; sonra 1-67 ile eğit, 68-74'ü tahmin et... scikit-learn'de bunu TimeSeriesSplit yapar.

from sklearn.model_selection import TimeSeriesSplit
for i, (egitim, test) in enumerate(TimeSeriesSplit(n_splits=3, test_size=7).split(X)):
    print(i, "eğitim:", X.index[egitim[0]].date(), "→", X.index[egitim[-1]].date(),
          "| test:", X.index[test[0]].date(), "→", X.index[test[-1]].date())

Kıyas modelleri: önce basit olanı yen

Her tahmin modelinin geçmesi gereken iki çıta vardır: naive (yarın = bugün) ve mevsimsel naive (yarın = geçen haftanın aynı günü). Modelin ortalama mutlak hatası (MAE) bunlardan iyi değilse karmaşıklık boşunadır.

test = gunluk.iloc[-14:]
naive = gunluk.shift(1).iloc[-14:]
mevsimsel = gunluk.shift(7).iloc[-14:]
print("naive MAE:", round((test - naive).abs().mean(), 1))
print("mevsimsel naive MAE:", round((test - mevsimsel).abs().mean(), 1))

Bu veride naive daha iyi çıkıyor; yani yolculuk sayısı haftalık örüntüden çok son günlerin düzeyine bağlı. Böyle bir bulgu, model kurmadan önce hangi özelliklerin önemli olacağını söyler. Zaman serisi tahmini dersinde bu kıyasları ExponentialSmoothing, SARIMA ve gecikme özellikli gradyan artırma modelleriyle aşmaya çalışacaksın.

Gerçek işteTalep tahmini (kaç bisiklet, kaç kurye, kaç kasiyer) operasyon ekiplerinin her gün kullandığı bir çıktıdır. Veri bilimi mülakatlarında "modelin testte çok iyi ama canlıda kötü, ne olabilir?" sorusunun ilk cevabı neredeyse her zaman zaman sızıntısıdır.
Uzmanlaş

resample'ın ince ayarları

resample("W") varsayılan olarak Pazar'a çapalanır ve etiketi aralığın sağ ucudur. Haftayı Pazartesi etiketiyle görmek istersen resample("W-MON", label="left", closed="left") kullan. Saatlik veride vardiya raporu için offset="8h" ile kovaları 08:00'dan başlatabilirsin.

Timestamp mi Period mu?

Timestamp bir an, Period bir aralıktır ("2024-03" ayının tamamı). Aylık raporlarda to_period("M") anahtarları hem okunur hem de ayın hangi günü etiketleneceği tartışmasını bitirir.

Tahmine giriş ve sızıntı

Tahmin (forecasting) için önce basit kıyas modellerini kur: "yarın = bugün" (naive) ve "gelecek salı = geçen salı" (seasonal naive). statsmodels'teki üstel düzeltme (ExponentialSmoothing) ve ARIMA bunların üstüne kurulur. Zaman serisinde rastgele train/test bölmesi yapma: modeli geçmişle eğit, gelecekle test et; aksi hâlde gelecekten bilgi sızar (leakage) ve model kâğıt üstünde parlak, gerçekte işe yaramaz çıkar.

Alıştırmalara geç (35)