Pandas GroupBy Rehberi: Veri Gruplama, Aggregation, Transform ve Filter

Pandas groupby() fonksiyonunu temel kullanımdan ileri düzey tekniklere kadar öğrenin. agg, transform, filter, named aggregation ve Pandas 3.0 performans ipuçlarıyla veri analizinizi güçlendirin.

Pandas GroupBy ile Veri Gruplama ve Analiz

Veri analiziyle uğraşıyorsanız şunu kesin biliyorsunuzdur: verileri kategorilere ayırıp her grup üzerinde ayrı ayrı hesaplamalar yapmak neredeyse kaçınılmaz bir ihtiyaç. İşte Pandas'ın groupby() fonksiyonu tam da bu noktada devreye giriyor. SQL'deki GROUP BY ifadesine aşinaysanız mantık aynı — ama Pandas çok daha esnek, bunu zamanla kendiniz de göreceksiniz.

Bu rehberde groupby() fonksiyonunun temellerinden başlayıp ileri düzey tekniklere kadar adım adım ilerleyeceğiz. Pandas 3.0 ile gelen performans iyileştirmelerini de atlamamak lazım, onlara da değineceğiz. Her bölümde çalışan kod örnekleri var, doğrudan kopyalayıp deneyebilirsiniz.

Split-Apply-Combine: GroupBy'ın Çalışma Mantığı

Pandas'ın groupby() fonksiyonu Split-Apply-Combine (Böl-Uygula-Birleştir) paradigmasına dayanıyor. Kulağa akademik gelebilir ama aslında oldukça basit bir süreç:

  1. Split (Böl): DataFrame, belirli bir sütundaki benzersiz değerlere göre alt gruplara ayrılıyor.
  2. Apply (Uygula): Her gruba bağımsız olarak bir fonksiyon uygulanıyor — toplama, dönüşüm, filtreleme, ne isterseniz.
  3. Combine (Birleştir): Sonuçlar tek bir veri yapısında birleştiriliyor.

Bu üç adımı kafanıza oturtursanız, GroupBy'ın geri kalanı çorap söküğü gibi gelir. Hadi somut örneklere geçelim.

Temel GroupBy Kullanımı

Önce basit bir örnek veri seti oluşturalım ve temel gruplama işlemlerini görelim:

import pandas as pd
import numpy as np

# Örnek satış verisi
np.random.seed(42)
n = 200
veri = pd.DataFrame({
    "sehir": np.random.choice(["İstanbul", "Ankara", "İzmir", "Bursa"], n),
    "kategori": np.random.choice(["Elektronik", "Giyim", "Gıda", "Kozmetik"], n),
    "satis_tutari": np.random.uniform(50, 5000, n).round(2),
    "adet": np.random.randint(1, 50, n),
    "tarih": pd.date_range("2025-01-01", periods=n, freq="D")
})

print(veri.head(10))
print(f"\nVeri boyutu: {veri.shape}")

Şimdi şehir bazında satış ortalamasını hesaplayalım:

# Şehir bazında ortalama satış tutarı
sehir_ortalama = veri.groupby("sehir")["satis_tutari"].mean()
print(sehir_ortalama)

# Birden fazla sütun seçerek gruplama
sehir_ozet = veri.groupby("sehir")[["satis_tutari", "adet"]].mean()
print(sehir_ozet)

Burada dikkat çekmek istediğim bir şey var: groupby() çağırdığınızda aslında henüz hiçbir hesaplama yapılmıyor. Sadece bir GroupBy nesnesi oluşuyor. Asıl hesaplama, üzerine .mean(), .sum() gibi bir toplama fonksiyonu uyguladığınızda gerçekleşiyor. Buna tembel değerlendirme (lazy evaluation) deniyor ve performans açısından oldukça akıllıca bir tasarım.

agg() ile Çoklu Aggregation İşlemleri

.agg() metodu, GroupBy'ın bence en güçlü silahı. Tek seferde birden fazla istatistik hesaplayabilmenizi sağlıyor ve bu gerçekten işleri kolaylaştırıyor.

Tek Sütunda Birden Fazla İstatistik

# Satış tutarı için birden fazla istatistik
satis_istatistik = veri.groupby("sehir")["satis_tutari"].agg(
    ["mean", "median", "std", "min", "max", "count"]
)
print(satis_istatistik)

Farklı Sütunlarda Farklı İstatistikler

# Her sütun için farklı hesaplamalar
detayli_ozet = veri.groupby("sehir").agg({
    "satis_tutari": ["sum", "mean", "std"],
    "adet": ["sum", "mean", "max"]
})
print(detayli_ozet)

Named Aggregation (İsimli Toplama)

Şahsen en çok sevdiğim özelliklerden biri Named Aggregation. Daha okunabilir ve düzenli sonuçlar üretiyor — özellikle sonuçları başka işlemlere aktarırken hayat kurtarıyor:

# Named aggregation ile daha temiz çıktılar
temiz_ozet = veri.groupby("sehir").agg(
    toplam_satis=("satis_tutari", "sum"),
    ortalama_satis=("satis_tutari", "mean"),
    max_adet=("adet", "max"),
    islem_sayisi=("satis_tutari", "count")
)
print(temiz_ozet)

Named aggregation'ın bir güzel tarafı da sütun adlarını kendiniz belirlediğiniz için o sinir bozucu MultiIndex sorunlarından kurtulmanız. Ciddi ciddi, MultiIndex ile uğraşmak istemiyorsanız bu yöntemi tercih edin.

Özel (Custom) Aggregation Fonksiyonları

Yerleşik fonksiyonlar bazen yetmiyor, bu durumda kendi fonksiyonlarınızı yazabilirsiniz:

# Değişim aralığını (range) hesaplayan özel fonksiyon
def degisim_araligi(x):
    return x.max() - x.min()

# Çeyrekler arası aralık (IQR) hesaplayan fonksiyon
def iqr(x):
    return x.quantile(0.75) - x.quantile(0.25)

ozel_istatistik = veri.groupby("kategori").agg(
    satis_araligi=("satis_tutari", degisim_araligi),
    satis_iqr=("satis_tutari", iqr),
    ortalama_satis=("satis_tutari", "mean")
)
print(ozel_istatistik)

Performans uyarısı: Özel fonksiyonlar (apply veya lambda ile) yerleşik metotlara kıyasla 5-50 kat daha yavaş olabiliyor. Mümkünse yerleşik metotları tercih edin — ciddiyim, fark çok büyük olabiliyor.

transform() ile Grup Bazlı Dönüşüm

transform() metodunu anlamak biraz kafa karıştırıcı olabilir ilk başta, ama kavrayınca "aa, ne güzel şeymiş" diyorsunuz. Temel farkı şu: agg() veriyi özetleyip küçültürken, transform() sonucu orijinal DataFrame ile aynı boyutta döndürüyor. Yani her satıra kendi grubunun istatistiğini atıyor.

Z-Skoru Normalizasyonu

# Her şehir grubu içinde satış tutarlarını z-skoru ile normalize etme
veri["satis_zskor"] = veri.groupby("sehir")["satis_tutari"].transform(
    lambda x: (x - x.mean()) / x.std()
)

print(veri[["sehir", "satis_tutari", "satis_zskor"]].head(10))

Grup Ortalaması ile Eksik Veri Doldurma

Bu, transform() için en yaygın kullanım alanlarından biri ve gerçekten de çok pratik:

# Eksik verileri grup ortalamasıyla doldurma
veri_eksik = veri.copy()
# Rastgele bazı satırları NaN yapalım
mask = np.random.choice([True, False], size=n, p=[0.1, 0.9])
veri_eksik.loc[mask, "satis_tutari"] = np.nan

print(f"Eksik değer sayısı: {veri_eksik['satis_tutari'].isna().sum()}")

# Her şehrin ortalamasıyla doldur
veri_eksik["satis_tutari"] = veri_eksik.groupby("sehir")["satis_tutari"].transform(
    lambda x: x.fillna(x.mean())
)

print(f"Doldurma sonrası eksik: {veri_eksik['satis_tutari'].isna().sum()}")

Grup İçi Yüzde Hesaplama

# Her satışın kendi şehri içindeki yüzdesini hesaplama
veri["sehir_yuzde"] = veri.groupby("sehir")["satis_tutari"].transform(
    lambda x: (x / x.sum()) * 100
)

print(veri[["sehir", "satis_tutari", "sehir_yuzde"]].head(10))

filter() ile Grup Bazlı Filtreleme

filter(), biraz farklı çalışıyor. Belirli bir koşulu karşılayan grupları bütünüyle tutuyor veya atıyor. Yani satır satır değil, grup grup filtreleme yapıyorsunuz. Normal filtrelemeden farkı tam olarak bu — karar grubun toplu özelliklerine göre veriliyor.

# Ortalama satış tutarı 2500 TL üzerinde olan şehirlerin tüm verilerini getir
yuksek_satis_sehirler = veri.groupby("sehir").filter(
    lambda x: x["satis_tutari"].mean() > 2500
)

print(f"Orijinal veri: {len(veri)} satır")
print(f"Filtrelenmiş: {len(yuksek_satis_sehirler)} satır")
print(f"Kalan şehirler: {yuksek_satis_sehirler['sehir'].unique()}")
# En az 55 işlemi olan şehirleri filtrele
aktif_sehirler = veri.groupby("sehir").filter(
    lambda x: len(x) >= 55
)

print(f"Aktif şehirler: {aktif_sehirler['sehir'].unique()}")

Çoklu Anahtar ile Gruplama

Tek sütuna göre gruplama güzel ama bazen yetmiyor. Birden fazla sütuna göre gruplama yaparak daha detaylı analizlere ulaşabilirsiniz:

# Şehir ve kategori bazında çapraz analiz
capraz_analiz = veri.groupby(["sehir", "kategori"]).agg(
    toplam_satis=("satis_tutari", "sum"),
    ortalama_satis=("satis_tutari", "mean"),
    islem_sayisi=("adet", "count")
).round(2)

print(capraz_analiz)

# MultiIndex sonucu düzleştirme
capraz_duz = capraz_analiz.reset_index()
print(capraz_duz.head(10))

Zaman Bazlı Gruplama: resample ve Grouper

Zaman serisi verileriyle çalışıyorsanız (ki veri analizinde bu kaçınılmaz), tarih bazlı gruplama yapmak çok sık gerekiyor. Pandas bunun için pd.Grouper ve resample araçlarını sunuyor:

# Tarih sütununu indeks olarak ayarlama
veri_ts = veri.set_index("tarih")

# Aylık satış toplamları
aylik_satis = veri_ts.groupby(pd.Grouper(freq="ME"))["satis_tutari"].sum()
print("Aylık Satışlar:")
print(aylik_satis)

# Şehir bazında aylık satışlar
sehir_aylik = veri_ts.groupby(
    ["sehir", pd.Grouper(freq="ME")]
)["satis_tutari"].sum().round(2)
print("\nŞehir Bazında Aylık Satışlar:")
print(sehir_aylik.head(12))
# resample ile haftalık ortalamalar
haftalik_ort = veri_ts["satis_tutari"].resample("W").agg(
    ["mean", "sum", "count"]
)
print(haftalik_ort.head(10))

GroupBy ile Sıralama ve Seçim

Her grup içinden belirli satırları seçmek veya sıralamak da sık karşılaşılan bir ihtiyaç. Mesela "her şehirdeki en yüksek 3 satış" gibi sorulara yanıt vermek istiyorsanız:

# Her şehirdeki en yüksek satışlı 3 kayıt
en_yuksek = veri.groupby("sehir").apply(
    lambda x: x.nlargest(3, "satis_tutari"),
    include_groups=False
).reset_index(level=0)
print(en_yuksek)

# Grup içi sıralama (rank)
veri["sehir_siralama"] = veri.groupby("sehir")["satis_tutari"].rank(
    method="dense", ascending=False
)
print(veri[["sehir", "satis_tutari", "sehir_siralama"]].head(10))

Pandas 3.0 ile GroupBy Performans İyileştirmeleri

Pandas 3.0, GroupBy işlemlerinde ciddi performans iyileştirmeleri getirdi. Eğer hâlâ Pandas 2.x kullanıyorsanız, bu bölümü okuduktan sonra güncelleme yapmak isteyeceksiniz.

Copy-on-Write (CoW) Varsayılan Davranış

Pandas 3.0'da Copy-on-Write artık varsayılan olarak aktif. Bunun pratikte anlamı şu: gereksiz bellek kopyaları oluşturulmuyor ve GroupBy işlemlerinde bellek kullanımı %40'a kadar düşebiliyor. Özellikle büyük veri setleriyle çalışıyorsanız bu fark ciddi anlamda hissediliyor.

PyArrow Destekli String Dtype

String sütunlar artık varsayılan olarak PyArrow tabanlı str dtype kullanıyor. Bu değişiklik, gruplama anahtarı olarak string sütun kullanan GroupBy işlemlerini 5-10 kat hızlandırıyor ve bellek tüketimini %50'ye kadar azaltıyor. Açıkçası bu rakamlar küçümsenecek gibi değil.

Otomatik Categorical Dönüşüm

String gruplama sütunları Pandas 3.0'da otomatik olarak categorical dtype'a dönüştürülüyor. Hash hesaplama yükünü azaltarak büyük veri setlerinde 2-3 kat hızlanma sağlıyor.

# Pandas 3.0 performans karşılaştırması
import time

# Büyük veri seti oluştur
np.random.seed(42)
buyuk_veri = pd.DataFrame({
    "grup": np.random.choice([f"Kategori_{i}" for i in range(100)], 1_000_000),
    "deger": np.random.randn(1_000_000)
})

# Standart groupby süresi
baslangic = time.perf_counter()
sonuc = buyuk_veri.groupby("grup")["deger"].agg(["mean", "sum", "std"])
sure = time.perf_counter() - baslangic
print(f"GroupBy süresi: {sure:.4f} saniye")

# Categorical dönüşüm ile hızlandırma
buyuk_veri["grup_cat"] = buyuk_veri["grup"].astype("category")
baslangic = time.perf_counter()
sonuc_cat = buyuk_veri.groupby("grup_cat")["deger"].agg(["mean", "sum", "std"])
sure_cat = time.perf_counter() - baslangic
print(f"Categorical GroupBy süresi: {sure_cat:.4f} saniye")

Performans İpuçları ve En İyi Uygulamalar

GroupBy işlemlerinden maksimum performans almak istiyorsanız şu önerilere göz atın. Bunları kendi projelerimde de uyguluyorum ve fark gerçekten belirgin:

  • Yerleşik metotları tercih edin: .sum(), .mean(), .count() gibi metotlar Cython ile derlenmiştir ve apply() ile lambda kullanımından 5-50 kat daha hızlıdır. Bu farkı küçümsemeyin.
  • Çoklu aggregation birleştirin: Ayrı ayrı .sum() ve .mean() çağırmak yerine .agg(["sum", "mean"]) kullanın. Gruplama adımı bir kez gerçekleşir, ciddi zaman kazandırır.
  • Categorical dtype kullanın: Gruplama anahtarınız sabit bir küme ise (şehirler, kategoriler gibi) sütunu .astype("category") ile dönüştürün.
  • Gruplama öncesi sıralama yapın: .sort_values("grup_sutunu") ile veriyi gruplama sütununa göre sıralamak, önbellek kullanımını iyileştirerek %15-25 hızlanma sağlayabiliyor.
  • PyArrow kurun: pip install pyarrow komutuyla PyArrow kurarak Pandas 3.0'ın string dtype optimizasyonlarından tam olarak yararlanın.
# Performans karşılaştırması: apply vs yerleşik metot
import time

buyuk = pd.DataFrame({
    "grup": np.random.choice(["A", "B", "C", "D"], 500_000),
    "deger": np.random.randn(500_000)
})

# Yavaş yol: apply ile
baslangic = time.perf_counter()
yavas = buyuk.groupby("grup")["deger"].apply(lambda x: x.sum())
sure_yavas = time.perf_counter() - baslangic

# Hızlı yol: yerleşik metot
baslangic = time.perf_counter()
hizli = buyuk.groupby("grup")["deger"].sum()
sure_hizli = time.perf_counter() - baslangic

print(f"apply ile: {sure_yavas:.4f}s")
print(f"Yerleşik .sum(): {sure_hizli:.4f}s")
print(f"Hız farkı: {sure_yavas/sure_hizli:.1f}x")

Gerçek Dünya Örneği: E-Ticaret Satış Analizi

Tamam, şimdiye kadar çok teknik konuştuk. Hadi tüm bu teknikleri bir araya getiren gerçekçi bir örnek yapalım — bir e-ticaret satış analizi:

import pandas as pd
import numpy as np

# E-ticaret veri seti simülasyonu
np.random.seed(2026)
n = 5000
eticaret = pd.DataFrame({
    "musteri_id": np.random.randint(1000, 2000, n),
    "kategori": np.random.choice(
        ["Elektronik", "Giyim", "Ev & Yaşam", "Spor", "Kitap"], n,
        p=[0.25, 0.3, 0.2, 0.15, 0.1]
    ),
    "sehir": np.random.choice(
        ["İstanbul", "Ankara", "İzmir", "Bursa", "Antalya"], n
    ),
    "tutar": np.random.exponential(500, n).round(2),
    "tarih": pd.date_range("2025-01-01", periods=n, freq="2h")
})

# 1. Kategori bazında kapsamlı özet
kategori_ozet = eticaret.groupby("kategori").agg(
    toplam_gelir=("tutar", "sum"),
    ortalama_siparis=("tutar", "mean"),
    siparis_sayisi=("tutar", "count"),
    benzersiz_musteri=("musteri_id", "nunique"),
    medyan_tutar=("tutar", "median")
).round(2)

kategori_ozet["musteri_basina_gelir"] = (
    kategori_ozet["toplam_gelir"] / kategori_ozet["benzersiz_musteri"]
).round(2)

print("Kategori Bazında Özet:")
print(kategori_ozet)

# 2. Her kategorideki satışı toplam içindeki yüzdeye dönüştürme
eticaret["kategori_payi"] = eticaret.groupby("kategori")["tutar"].transform(
    lambda x: x / x.sum() * 100
).round(2)

# 3. Yalnızca 100 üzerinde sipariş alan şehirleri analiz etme
aktif_sehirler = eticaret.groupby("sehir").filter(
    lambda x: len(x) >= 100
)

# 4. Aylık trend analizi
eticaret_ts = eticaret.set_index("tarih")
aylik_trend = eticaret_ts.groupby(
    ["kategori", pd.Grouper(freq="ME")]
).agg(
    aylik_gelir=("tutar", "sum"),
    siparis=("tutar", "count")
).round(2)

print("\nAylık Trend (ilk 15 satır):")
print(aylik_trend.head(15))

Sık Sorulan Sorular

Pandas groupby ile SQL GROUP BY arasındaki fark nedir?

Temel mantık aynı olsa da Pandas groupby çok daha esnek. SQL'de yalnızca aggregation yapabilirsiniz, ama Pandas'ta bunun yanında transform (orijinal boyutta dönüşüm) ve filter (grup bazlı filtreleme) gibi farklı işlem tipleri de var. Pandas'ta özel fonksiyonlar yazıp gruplara uygulayabilirsiniz — bu SQL'de o kadar kolay değil. Bir diğer fark da Pandas'ın NaN değerleri varsayılan olarak gruplardan çıkarması; SQL ise NULL'ı ayrı bir grup olarak ele alır.

groupby().apply() neden yavaş ve ne zaman kullanmalıyım?

apply() her grup için ayrı bir Python fonksiyonu çağrısı yapıyor, bu da Cython ile derlenmiş yerleşik metotlara kıyasla 5-50 kat daha yavaş olmasına neden oluyor. Tavsiyem şu: apply() 'ı yalnızca yerleşik metotlarla karşılayamadığınız karmaşık işlemler için kullanın. Basit toplam veya ortalama hesaplamaları için .sum() ve .mean() her zaman daha iyi bir tercih.

transform() ve agg() arasındaki fark nedir?

agg() veriyi özetler, grup başına tek satır döndürür — sonuç orijinalden küçük bir DataFrame olur. transform() ise hesaplanan değeri orijinal DataFrame ile aynı boyutta geri döndürür, yani her satıra kendi grubunun istatistiğini atar. Grup ortalamasıyla normalizasyon veya eksik veri doldurma gibi işlemler için transform() tam aradığınız şey.

Pandas 3.0 groupby performansını nasıl artırdı?

Üç ana alanda iyileştirme var: Copy-on-Write ile gereksiz bellek kopyalarının önlenmesi (%40 bellek tasarrufu), PyArrow destekli string dtype ile string gruplama işlemlerinin 5-10 kat hızlanması ve otomatik categorical dönüşüm ile hash hesaplama yükünün azaltılması. Büyük veri setlerinde GroupBy işlemleri Pandas 2.2'ye kıyasla yaklaşık 1.5-2 kat daha hızlı çalışıyor.

Büyük veri setlerinde groupby performansını nasıl artırabilirim?

Beş temel strateji var: (1) Gruplama sütunlarını .astype("category") ile categorical'a dönüştürün, (2) apply() yerine yerleşik metotları kullanın, (3) Çoklu aggregation'ları tek bir .agg() çağrısında birleştirin, (4) Gruplama öncesi veriyi ilgili sütuna göre sıralayın, (5) PyArrow'u kurun ve Pandas 3.0 kullanın. Bu beşini birlikte uyguladığınızda fark gerçekten çarpıcı oluyor.

Yazar Hakkında Editorial Team

Our team of expert writers and editors.