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.
| İfade | Sonuç (2024-03-04 Pazartesi) | Kullanım |
|---|---|---|
.dt.year, .dt.month, .dt.day | 2024, 3, 4 | Yıl/ay kırılımı |
.dt.dayofweek | 0 (Pazartesi=0, Pazar=6) | Hafta içi/sonu analizi |
.dt.quarter | 1 | Çeyrek raporu |
.dt.hour | 0 | Saatlik yoğunluk |
.dt.to_period("M") | 2024-03 | Ay anahtarı (raporda gruplama) |
.dt.normalize() | 2024-03-04 00:00 | Saati at, günü tut |
.dt.is_month_end | False | Ay 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())
dt.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 ad | Anlamı | Etiket | Not |
|---|---|---|---|
"D" | Gün | O gün | Her sürümde aynı |
"W" | Hafta (Pazartesi–Pazar) | Pazar günü | "W-MON" gibi çapa seçilebilir |
"MS" | Ay | Ayın ilk günü | Rapor için en okunur |
"ME" | Ay | Ayın son günü | pandas 2.2+; eski "M" kaldırılıyor |
"QS" / "QE" | Çeyrek | Başı / sonu | Eski "Q" yerine |
"h" | Saat | Saat 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", "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) | 0 | Satış adedi, işlem sayısı, ciro |
| Durum (stock) | Son bilinen değer (ffill) | Stok, hesap bakiyesi, döviz kuru |
| Ölçüm | Ara değer (interpolate) ya da boş bırak | Sı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())
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.
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())))
dt.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]])
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:
| Özellik | pandas | Anlamı |
|---|---|---|
| lag_1 | s.shift(1) | Dünkü değer |
| lag_7 | s.shift(7) | Geçen haftanın aynı günü |
| ort_7 | s.shift(1).rolling(7).mean() | Dünden geriye son 7 günün ortalaması |
| haftanin_gunu | s.index.dayofweek | Takvimden gelir; gelecek için de bilinir |
s.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.
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.