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

İleri pandas: şekil, pencere ve zincir

MultiIndex, melt/pivot ile yeniden şekillendirme, rolling/expanding/ewm pencereleri, rank ve cumcount, kategorik tür, assign/pipe/query zinciri, np.select ile vektörleştirme ve validate/indicator ile güvenli merge.

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

Neden "ileri" pandas?

Seçme, filtreleme ve temizliği pandas giriş ile veri temizleme derslerinde, groupby, pivot_table ve merge'ü groupby, pivot, merge dersinde gördün. Gerçek işte bunlar yetmeye başladığı an şöyle sorular gelir: "Her şehirde ciroya göre ilk 3 ürün hangisi?", "Excel'den gelen ay sütunlarını nasıl tek sütuna çeviririm?", "Son 7 günün hareketli ortalaması düşüyor mu?", "Birleştirmeden sonra neden satır sayım arttı?". Bu ders, analistin günlük işindeki bu ikinci katmanı kapsar: çok seviyeli index (MultiIndex), yeniden şekillendirme (reshaping), pencere fonksiyonları (window functions), sıralama, kategorik tür, zincirleme yazım (method chaining), vektörleştirme (vectorization) ve güvenli birleştirme.

Tüm örnekler Anadolu Market'in küçük bir satış tablosu üzerinde. Kodları çalıştır, çıktıyı oku, sonra bir değeri değiştirip tekrar çalıştır; ileri pandas en hızlı böyle oturur.

MultiIndex: iki anahtarlı index

Birden çok sütunla gruplayınca ya da set_index(["sehir", "ay"]) yazınca index iki seviyeli olur. Her satırın adresi artık bir demettir (tuple): ("Ankara", 2). Bu yapı "şehir içinde ay" gibi hiyerarşik veriyi doğal biçimde taşır.

import pandas as pd

df = pd.DataFrame({
    "sehir": ["Ankara", "Ankara", "İzmir", "İzmir", "Bursa", "Bursa"],
    "ay": [1, 2, 1, 2, 1, 2],
    "ciro": [1200, 1500, 800, 950, 600, 720],
}).set_index(["sehir", "ay"]).sort_index()

print(df)
print(df.loc[("Ankara", 2), "ciro"])          # tek hücre: demetle adresle
print(df.loc["İzmir"])                          # dış seviyeyle seç
print(df.xs(1, level="ay"))                     # iç seviyeden kesit: tüm şehirlerin 1. ayı
print(df.swaplevel().sort_index().head(3))      # seviyelerin yerini değiştir
print(df["ciro"].unstack("ay"))                 # iç seviyeyi sütunlara aç

xs (cross-section) iç seviyeden seçim yapmanın en okunur yoludur. swaplevel seviyelerin sırasını değiştirir; ardından mutlaka sort_index() çağır, çünkü sırasız MultiIndex üzerinde dilimleme hem yavaştır hem de UnsortedIndexError verebilir. unstack bir seviyeyi sütunlara açar, stack tersini yapar.

Ne zaman MultiIndex, ne zaman düz tablo?Ara hesaplarda MultiIndex güçlüdür (unstack, xs). Rapor, CSV, Excel ya da başka bir sisteme gidecek tabloda reset_index() ile düz sütunlara dön; çoğu araç MultiIndex'i anlamaz.

Yeniden şekillendirme: geniş ↔ uzun

Aynı veri iki biçimde durabilir. Geniş (wide) biçimde her ay bir sütundur; insan okumak için rahattır, Excel raporları böyle gelir. Uzun (long, tidy) biçimde her satır tek bir gözlemdir: şehir, ay, ciro. Gruplama, grafik (özellikle seaborn) ve veritabanı uzun biçimi sever.

İşlemYönNe zaman
melt(id_vars, var_name, value_name)geniş → uzunExcel'den gelen ay/ürün sütunlarını tek sütuna indirmek
pivot(index, columns, values)uzun → genişAnahtar çiftleri tekilse; yinelenen varsa hata verir
pivot_table(..., aggfunc)uzun → genişYinelenen çiftler toplanacaksa (sum, mean)
stack() / unstack()sütun ↔ index seviyesiMultiIndex ile çalışırken
import pandas as pd

genis = pd.DataFrame({
    "sehir": ["Ankara", "İzmir"],
    "Oca": [1200, 800], "Şub": [1500, 950], "Mar": [1400, 1010],
})
uzun = genis.melt(id_vars="sehir", var_name="ay", value_name="ciro")
print(uzun)

geri = uzun.pivot(index="sehir", columns="ay", values="ciro")
print(geri[["Oca", "Şub", "Mar"]])          # sütun sırasını açıkça ver

# Aynı şehir-ay çifti iki kez varsa pivot hata verir, pivot_table toplar
uzun2 = pd.concat([uzun, pd.DataFrame({"sehir": ["Ankara"], "ay": ["Oca"], "ciro": [100]})])
print(uzun2.pivot_table(index="sehir", columns="ay", values="ciro", aggfunc="sum"))
pivot ile pivot_table farkıpivot yalnızca yeniden düzenler; aynı (index, columns) çifti iki kez geçerse ValueError: Index contains duplicate entries verir. Bu hata çoğu zaman veride beklemediğin bir yineleme olduğunu söyler: önce nedenini bul, sonra bilinçli olarak pivot_table(aggfunc="sum") seç.

Pencere fonksiyonları: rolling, expanding, ewm

Günlük ciro gürültülüdür; eğilimi görmek için hareketli ortalama (rolling mean) kullanılır. rolling(3) son 3 satırlık bir pencere kaydırır; pencere dolana kadar sonuç NaN olur, min_periods=1 ile ilk satırlarda da değer alırsın. expanding() başlangıçtan bugüne biriken pencere (kümülatif ortalama), ewm(span=3) ise yakın geçmişe daha çok ağırlık veren üstel ağırlıklı ortalamadır (exponentially weighted).

import pandas as pd

satis = pd.DataFrame({
    "sube": ["Kadıköy"] * 5 + ["Çankaya"] * 5,
    "gun": list(range(1, 6)) * 2,
    "adet": [10, 14, 9, 20, 18, 5, 7, 6, 9, 12],
})
satis["ort3"] = (satis.groupby("sube")["adet"]
                 .transform(lambda s: s.rolling(3, min_periods=1).mean())
                 .round(2))
satis["kum_ort"] = satis.groupby("sube")["adet"].transform(lambda s: s.expanding().mean()).round(2)
satis["ewm"] = satis.groupby("sube")["adet"].transform(lambda s: s.ewm(span=3).mean()).round(2)
print(satis)

Grup içinde pencere istiyorsan (her şube kendi geçmişine baksın) groupby(...).transform(lambda s: s.rolling(...)) kalıbını kullan. Gruplamadan rolling çağırırsan Kadıköy'ün son günleri Çankaya'nın ilk gününe karışır; bu, en sık yapılan pencere hatasıdır.

Sıralama, grup içi sıra ve kümülatif işlemler

rank değerlere sıra numarası verir. Eşitlikleri nasıl ele aldığı method ile seçilir. SQL'deki ROW_NUMBER, RANK, DENSE_RANK karşılıkları buradadır.

method[300, 200, 200, 100] azalanSQL karşılığı
"first"1, 2, 3, 4ROW_NUMBER
"min"1, 2, 2, 4RANK
"dense"1, 2, 2, 3DENSE_RANK
"average" (varsayılan)1, 2.5, 2.5, 4—
import pandas as pd

urun = pd.DataFrame({
    "sehir": ["Ankara", "Ankara", "Ankara", "İzmir", "İzmir", "İzmir"],
    "urun": ["Çay", "Kahve", "Süt", "Çay", "Kahve", "Süt"],
    "ciro": [500, 900, 500, 300, 250, 700],
})
urun["sira"] = urun.groupby("sehir")["ciro"].rank(method="dense", ascending=False).astype(int)
print(urun.sort_values(["sehir", "sira"]))
print(urun[urun["sira"] == 1])            # her şehrin bir numarası (eşitse ikisi de)

siparis = pd.DataFrame({"musteri": ["A", "B", "A", "A", "B"], "tutar": [100, 50, 70, 30, 20]})
siparis["kacinci"] = siparis.groupby("musteri").cumcount() + 1
siparis["kumulatif"] = siparis.groupby("musteri")["tutar"].cumsum()
print(siparis)

cumcount grup içinde 0'dan başlayan sıra numarasıdır; "müşterinin kaçıncı siparişi?" sorusunun cevabıdır ve "ilk siparişi olanları" bulmak için idealdir. cumsum, cummax, cumprod ise kümülatif değerleri verir. Bunlar satır sırasına bağlıdır: önce tarihe göre sırala.

Kategorik tür ve bellek

Bir milyon satırda "İstanbul" metni bir milyon kez saklanır. category türü her farklı değeri bir kez saklar, satırlarda yalnızca küçük tam sayı kodları tutar. Az sayıda farklı değeri olan sütunlarda (şehir, kategori, durum) bellek ciddi düşer ve groupby hızlanır. Sıralı kategori (ordered=True) ise "Bronz < Gümüş < Altın" gibi anlamlı sıralama ve karşılaştırma sağlar.

import pandas as pd

n = 30_000
df = pd.DataFrame({
    "sehir": ["İstanbul", "Ankara", "İzmir"] * (n // 3),
    "segment": ["Bronz", "Altın", "Gümüş"] * (n // 3),
})
once = df.memory_usage(deep=True).sum()
df["sehir"] = df["sehir"].astype("category")
seg_tur = pd.CategoricalDtype(["Bronz", "Gümüş", "Altın"], ordered=True)
df["segment"] = df["segment"].astype(seg_tur)
sonra = df.memory_usage(deep=True).sum()
print(f"bellek: {once:,} → {sonra:,} bayt")
print((df["segment"] >= "Gümüş").sum(), "satır Gümüş ve üstü")
print(df["segment"].cat.categories.tolist())

Kategorik sütunla groupby yaparken observed=True ver; aksi hâlde veride olmayan kategoriler de sıfır satırı olarak gelir.

Zincirleme yazım: assign, pipe, query

Ara değişkenlerle dolu bir defter yerine tek bir okunur boru hattı (pipeline) yazabilirsin. assign yeni sütun ekler ve yeni DataFrame döndürür; içinde lambda d: kullanınca bir önceki adımın sonucuna erişirsin. query filtreyi metin olarak yazar (@ ile Python değişkenine erişir). pipe kendi fonksiyonunu zincirin ortasına takar.

import pandas as pd

df = pd.DataFrame({
    "sehir": ["Ankara", "İzmir", "Ankara", "Bursa", "İzmir"],
    "adet": [3, 1, 5, 2, 4],
    "fiyat": [120.0, 899.0, 45.5, 250.0, 129.9],
})
esik = 200

def kdv_ekle(d, oran=0.20):
    return d.assign(kdvli=lambda x: (x["tutar"] * (1 + oran)).round(2))

rapor = (
    df
    .assign(tutar=lambda d: d["adet"] * d["fiyat"])
    .query("tutar > @esik")
    .pipe(kdv_ekle, oran=0.20)
    .sort_values("tutar", ascending=False)
    .reset_index(drop=True)
)
print(rapor)

Zincir, her adımın girdisini değiştirmediği için yan etkisizdir; hata ayıklarken herhangi bir satırı yoruma alıp sonucu görebilirsin. Çok uzayan zinciri anlamlı adlı pipe fonksiyonlarına böl.

apply mı, vektörleştirme mi, np.where/np.select mi?

apply(axis=1) her satır için Python fonksiyonu çağırır; okunur ama yavaştır. Aynı işi sütun aritmetiği, np.where (iki seçenek) ya da np.select (çok koşul) ile yazmak genellikle onlarca kat hızlıdır.

import numpy as np
import pandas as pd
import time

rng = np.random.default_rng(0)
df = pd.DataFrame({"tutar": rng.integers(10, 2000, 100_000)})

t0 = time.perf_counter()
yavas = df.apply(lambda r: "Altın" if r["tutar"] >= 1000 else ("Gümüş" if r["tutar"] >= 500 else "Bronz"), axis=1)
t1 = time.perf_counter()
kosullar = [df["tutar"] >= 1000, df["tutar"] >= 500]
hizli = np.select(kosullar, ["Altın", "Gümüş"], default="Bronz")
t2 = time.perf_counter()
print("aynı mı:", (yavas.to_numpy() == hizli).all())
print(f"apply: {t1 - t0:.3f} sn, np.select: {t2 - t1:.4f} sn")
print("iki seçenek:", np.where(df["tutar"].head(3) >= 1000, "büyük", "küçük"))
AraçHızKullan
Sütun aritmetiği, .str, .dtEn hızlıHer zaman ilk tercih
np.where / np.selectÇok hızlıKoşula göre değer atama
Series.map(sözlük)HızlıKod → ad eşleme
apply(axis=1)YavaşGerçekten satır satır mantık (dış API, karmaşık metin)
iterrowsEn yavaşNeredeyse hiç; gerekirse itertuples

Güvenli birleştirme: validate, indicator, yinelenen anahtar

merge'ün en tehlikeli sessiz hatası, sağ tabloda anahtarın yinelenmesidir: siparişler çoğalır, ciro şişer ve kimse fark etmez. validate beklentini yazar ve bozulursa MergeError verir. indicator=True her satıra _merge sütunu ekler: both, left_only, right_only.

import pandas as pd

siparis = pd.DataFrame({"siparis_id": [1, 2, 3, 4], "musteri_id": [10, 20, 10, 99], "tutar": [150, 80, 60, 40]})
musteri = pd.DataFrame({"musteri_id": [10, 20, 20], "ad": ["Ayşe", "Mehmet", "Mehmet (eski kayıt)"]})

print("yinelenen anahtar:", musteri["musteri_id"].duplicated().sum())
try:
    pd.merge(siparis, musteri, on="musteri_id", how="left", validate="many_to_one")
except pd.errors.MergeError as e:
    print("Yakalandı:", e)

musteri = musteri.drop_duplicates("musteri_id", keep="first")
b = pd.merge(siparis, musteri, on="musteri_id", how="left", validate="many_to_one", indicator=True)
print(b)
print("eşleşmeyen sipariş:", b.loc[b["_merge"] == "left_only", "siparis_id"].tolist())
print("satır sayısı korundu mu:", len(b) == len(siparis))

validate değerleri: "one_to_one", "one_to_many", "many_to_one", "many_to_many". Sipariş (çok) → müşteri (tek) için "many_to_one" yazılır. Birleştirmeden sonra len karşılaştırması da ucuz ve hayat kurtaran bir kontroldür.

Zincirleme atama çalışmazdf[df["sehir"] == "Ankara"]["indirim"] = 0.1 önce bir kopya üretir, atama o kopyaya gider ve asıl tablo değişmez. pandas 2'de uyarı, pandas 3'te (Copy-on-Write) yine uyarı ve hiçbir etki. Doğrusu tek adımda: df.loc[maske, "indirim"] = 0.1.
Gerçek işteBir perakende analiz ekibinde aylık "şehir × kategori" raporu genellikle şu boru hattıdır: ham sipariş tablosu → müşteri ve ürün tablolarıyla validate'li merge → assign ile tutar → pivot_table → şehir sıralaması için rank → reset_index ile Excel'e. Her birleştirmeden sonra satır sayısını loglamak ve indicator ile eşleşmeyenleri ayrı bir "veri kalitesi" sekmesine yazmak, raporun güvenilirliğini yöneticinin gözünde belirleyen alışkanlıktır.
Uzmanlaş

Performans ipuçları

  • Okurken küçült: read_csv(usecols=[...], dtype={"sehir": "category", "adet": "int32"}) gereksiz sütunu hiç yüklemez ve doğru türle başlar.
  • Tür daralt: pd.to_numeric(s, downcast="integer") int64'ü sığıyorsa int8/int16/int32'ye indirir.
  • groupby'da sort=False: sonuç sırası önemli değilse sıralama maliyetinden kurtulursun.
  • Döngü gerekiyorsa itertuples: iterrows'tan birkaç kat hızlıdır, çünkü her satır için Series üretmez.
  • Önce ölç: %timeit ya da time.perf_counter ile ölçmeden optimize etme; çoğu zaman darboğaz tek bir apply'dır.
  • Sınırı bil: Veri belleğe sığmıyorsa parça parça oku (chunksize) ya da sütunlu motorlara (polars, DuckDB) geç; pandas'ta pyarrow destekli türler de bellek kazandırır.

Grup içi ilk N

"Her şehirde en çok satan 3 ürün" için iki yol vardır: sort_values("ciro", ascending=False).groupby("sehir").head(3) (hızlı, eşitlikte keyfi) ya da rank(method="dense") ile sira <= 3 filtresi (eşitlikleri birlikte tutar). İş kuralı hangisini istiyorsa onu seç ve raporda belirt.

Alıştırmalara geç (18)