Veri analizi öğrenirken pandas'ta öyle bir noktaya geliyorsun ki, aynı işi 3 farklı fonksiyonla yapabildiğini fark ediyorsun. "Hangisi doğru?" sorusu beni çok yordu zamanında. Bu yazıda kendi karışıklıklarımı ve onları nasıl çözdüğümü paylaşıyorum.
Pandas, Python'ın veri analizi alanındaki en popüler kütüphanesi. Ama tam da popülerliği yüzünden, aynı işi farklı yollarla yapmanı sağlayan bir sürü fonksiyon var. Yeni başlayan birinin kafası karışıyor: "merge mi join mi?", "apply mı map mi?", "loc mu iloc mu?" Hepsine birlikte bakacağız şimdi.
Ben de son aylarda farklı veri analizi projeleri üzerinde çalışırken bu fonksiyonların hangisini ne zaman kullanmam gerektiğini deneme-yanılma yoluyla öğrendim. Bu yazıda, en sık karıştırılan 7 fonksiyon grubunu gerçek durumlar üzerinden anlatacağım.
Örnek veriler Netflix kataloğu analiz projemden — yaklaşık 8.800 satırlık bir veri seti, ~10 sütunlu, içinde tarih, kategorik ve metin verileri var. Netflix izleyicisi olarak veriyi incelemek özellikle keyifliydi.
Bu yazıdaki tüm kod örnekleri Pandas 3.0 üzerinde test edildi. Eğer hâlâ Pandas 1.x veya 2.x kullanıyorsanız bazı davranışlar farklı olabilir, sonuna doğru bunlardan bahsediyorum.
Durum 1: Veri Seçerken — loc vs iloc
Bu, en sık karıştırılan ikili. Yeni başlayan herkes ilk zamanlarda bunu sorar.
Temel fark çok basit aslında:
iloc → pozisyon ile seçer (0, 1, 2... gibi tamsayı pozisyon)
loc → etiket ile seçer (sütun adı veya index değeri)
Netflix verisinde ilk 3 satırın ilk 2 sütununu seçmek istediğimi düşün:
# iloc — pozisyon mantığı
df.iloc[0:3, 0:2]
# loc — etiket mantığı
df.loc[0:2, ['show_id', 'type']]
İki satır da aynı sonucu verir ama dikkatli ol:
Tuzak 1 — slice davranışı: iloc[0:3] Python slice mantığında çalışır (son dahil değil), yani satır 0, 1, 2'yi alır. loc[0:2] ise etiket mantığında çalışır ve son etiket dahildir, yani 0, 1, 2 etiketli satırları alır. Sonuçta her ikisi de 3 satır döner ama farklı mantıkla.
Bu davranış farkı, default integer index'lerde çoğu zaman aynı sonuca götürür. Ama index'i set_index('show_id') ile değiştirdiğinde işler değişir:
df_indexed = df.set_index('show_id')
df_indexed.iloc[0] # Pozisyon olarak ilk satır
df_indexed.loc['s1'] # Label'i 's1' olan satır
Tuzak 2 — koşullu filtreleme: En sık loc koşullu filtrelemede kullanılır:
# Sadece filmleri ve title, release_year sütunlarını seç
movies = df.loc[df['type'] == 'Movie', ['title', 'release_year']]
Ne zaman hangisi?
- Tam pozisyonla erişmem gerekiyor (1., 2., 3. satır) → iloc
- Sütun adlarını biliyorum, etiketle erişeyim → loc
- Koşullu filtreleme yapacağım → loc
Koşullu filtrelemede her zaman loc kullanmak alışkanlık haline gelmeli — okuyucu için niyeti açık ifade eder.
Durum 2: Tabloları Birleştirirken — merge vs join vs concat
Üç farklı fonksiyon, üç farklı senaryo. Kafa karıştırıcı geliyor farkındayım.
concat → tabloları alt alta veya yan yana yapıştırır. Birleştirme anahtarı falan yok, sadece yapıştırma işlemi.
# İki ay önce gelen veriyi yenisiyle birleştir (alt alta)
df_all = pd.concat([df_old, df_new], axis=0)
# Aynı satırlara yeni sütunlar ekle (yan yana)
df_combined = pd.concat([df1, df2], axis=1)
merge → SQL'deki JOIN gibi çalışır. Ortak bir sütuna göre birleştirir.
# Netflix filmleri ile yönetmen ek bilgilerini birleştir
directors_info = pd.DataFrame({
'director': ['Wes Anderson', 'Christopher Nolan', 'Akira Kurosawa'],
'country_of_birth': ['USA', 'UK', 'Japan']
})
df_enriched = df.merge(directors_info, on='director', how='left')
merge çok güçlü çünkü 4 farklı how parametresi destekliyor:
- inner (varsayılan): sadece her iki tabloda da eşleşenler
- left: sol tablodaki her şey + sağdan eşleşenler
- right: tersi
- outer: her ikisinden de tüm satırlar
join → aslında merge'ün özel bir hali. Sadece index üzerinden birleştirme yapar. Default olarak left join çalışır.
df1 = pd.DataFrame({'A': [1,2,3]}, index=['a','b','c'])
df2 = pd.DataFrame({'B': [4,5]}, index=['a','b'])
df1.join(df2) # default: left join (3 satır döner, 'c' için B=NaN)
Pratik bir kural.Ben aklıma şöyle kodladım:
1. Yapıştırma mı yapıyorum? → concat
2. Ortak bir sütun (mesela 'director', 'customer_id') üzerinden birleştireceğim → merge
3. Index üzerinden birleştireceğim → join
Ben artık join yerine her zaman merge kullanıyorum. join aslında merge(left_index=True, right_index=True)'ın kısaltması — daha az yazılıyor ama o kadar. merge her senaryoyu kapsadığı için akılda tutması daha kolay.
Durum 3: Sütunları Dönüştürürken — apply vs map (ve DataFrame.map)
Bu üçlü, yeni başlayan birinin gözünde aynı şey gibi görünür ama aslında farklı amaçlar için kullanılırlar.
Series.map → adı üstünde, sadece Series üzerinde çalışır. Tek bir sütunun değerlerini başka bir şeye dönüştürmek için.
Netflix verisinde içerik tiplerini Türkçeye çevirelim:
type_translation = {'Movie': 'Film', 'TV Show': 'Dizi'}
df['type_tr'] = df['type'].map(type_translation)
map sözlük (dictionary), Series veya fonksiyon alır. Her zaman Series üzerinde çalışır.
apply → hem Series hem DataFrame üzerinde çalışabilir. Daha esnek.
Series üzerinde:
df['title_length'] = df['title'].apply(len)
DataFrame üzerinde — satır satır veya sütun sütun işlem yapar:
# Her sütunun null sayısını bul
df.apply(lambda col: col.isnull().sum(), axis=0)
# Her satırı bir fonksiyondan geçir
df['summary'] = df.apply(lambda row: f"{row['type']} - {row['title']}", axis=1)
DataFrame.map → DataFrame'in her hücresine ayrı ayrı bir fonksiyon uygular.
# Tüm string sütunları küçük harfe çevir
df_strings = df[['type', 'rating']].map(lambda x: str(x).lower())
Pandas 2.1'e kadar bu işlevin adı applymap'ti. Pandas 2.1'de DataFrame.map olarak yeniden adlandırıldı (eskisi deprecated olarak işaretlendi), Pandas 3.0'da ise applymap tamamen kaldırıldı. Yani eski Stack Overflow cevaplarında applymap görürseniz, modern pandas'ta DataFrame.map olarak yazmanız gerekiyor.
Ne zaman hangisi?
- Tek sütundaki değerleri sözlük ile değiştireceğim → Series.map
- Tek sütuna karmaşık bir fonksiyon uygulayacağım → apply (Series üzerinde)
- Satır satır veya sütun sütun iş yapacağım → apply (DataFrame üzerinde)
- Tüm hücrelere aynı işlemi uygulayacağım → DataFrame.map
Performans notu: Sayısal operasyonlarda vektörize işlem apply'dan dramatik şekilde hızlıdır — 1 milyon satırlık bir Series üzerinde test ettiğimde direkt n * 2 işlemi yaklaşık 50 kat daha hızlı çıkıyor. String operasyonlarında fark daha az: bazıları (.str.len()) belirgin şekilde hızlı, bazıları (.str.upper()) apply ile benzer hızda. Yine de okunabilirlik için vektörize/.str formu tercih edilir:
# Sayısal operasyon — dramatik fark
df['double_year'] = df['release_year'] * 2 # ~50x hızlı
df['double_year'] = df['release_year'].apply(lambda x: x * 2) # yavaş
# String operasyon — fark daha az ama .str daha okunabilir
df['title_upper'] = df['title'].str.upper()
df['title_len'] = df['title'].str.len() # .str.len() apply'dan ~3x hızlı
Genel kural: Yapabiliyorsan vektörize yaz, kod hem daha hızlı hem daha okunabilir olur.
Durum 4: Gruplama Sonrası — agg vs apply vs transform
groupby sonrası bu üç fonksiyon arasında seçim yapmak en kafa karıştırıcı kısımlardan biri. Aralarındaki farkı bir cümleyle özetleyeyim:
- agg → her grup için tek bir değer üretir (özet istatistik)
- transform → her satır için aynı uzunlukta bir sonuç döner (broadcast)
- apply → ne istersen yapabilir, en esnek olan
Netflix verisinde rating bazında ortalama yayın yılı çıkarmak istiyorum:
df.groupby('rating')['release_year'].agg('mean')
Sonuç şuna benzer çıkar (gerçek sayılar verinize göre değişir):
rating
NR 2011.92
PG 2012.04
PG-13 2011.86
R 2012.31
TV-14 2012.06
TV-MA 2012.11
...
Her grup için tek bir değer döner. DataFrame küçüldü, gruplara göre özetlendi.
Şimdi transform'a bakalım — her filmin yılını, kendi rating grubunun ortalamasından farkını hesaplamak istiyorum:
df['mean_year_by_rating'] = df.groupby('rating')['release_year'].transform('mean')
df['year_diff'] = df['release_year'] - df['mean_year_by_rating']
Burada transform aynı sayıda satır döner — orijinal DataFrame'le hizalı. Her satır için, satırın ait olduğu grubun ortalaması broadcast edilir.
apply ise her ikisinin de yapabildiklerini yapar ama daha esnek:
# Her rating grubunun en eski filmini bul
df.groupby('rating', group_keys=False).apply(
lambda g: g.nsmallest(1, 'release_year')
)
Bunu agg ile yapamazsın çünkü dönen değer tek bir sayı değil, bir satır.
Pratik kural:
- "Her grup için bir özet sayı istiyorum" → agg
- "Her satır için grup-bazlı bir değer istiyorum" → transform
- "Daha karmaşık bir şey istiyorum" → apply
Durum 5: Eksik Veri — fillna vs dropna vs interpolate (ve ffill / bfill)
Eksik veriyle ne yapacağına karar vermek, bir veri analistinin en sık verdiği kararlardan biri. Birkaç temel strateji var.
dropna → eksik verisi olan satırları (veya sütunları) sil.
# director sütunu eksik olan satırları sil
df_clean = df.dropna(subset=['director'])
# Herhangi bir sütunu null olan satırı sil
df_clean = df.dropna()
# Tamamı null olan satırı sil
df_clean = df.dropna(how='all')
Netflix verisinde director sütununda eksik kayıtlar bulunabilir (örnek veri setinde bir kısmı boş). Hepsini silmek veri kaybı demek. O zaman:
fillna → eksik verileri bir değerle doldur.
# Bilinmeyen yönetmenleri "Unknown" olarak işaretle
df['director'] = df['director'].fillna('Unknown')
# Sayısal sütunlarda medyan ile doldur
df['some_score'] = df['some_score'].fillna(df['some_score'].median())
ffill ve bfill → önceki veya sonraki değerle doldur (zaman serisinde işe yarar).
# Önceki geçerli değerle doldur
df['daily_views'] = df['daily_views'].ffill()
# Sonraki geçerli değerle doldur
df['daily_views'] = df['daily_views'].bfill()
Pandas 2.x'te df.fillna(method='ffill') yazmak da çalışıyordu. Ama Pandas 3.0'da method parametresi tamamen kaldırıldı, artık doğrudan .ffill() veya .bfill() kullanman gerekiyor.
interpolate → eksik değerleri etrafındaki değerlerden tahmin et. Özellikle zaman serisi verilerinde işe yarar.
# Lineer interpolasyon (default)
df['daily_views'] = df['daily_views'].interpolate()
# Polinom interpolasyon (scipy gerektirir: pip install scipy)
df['daily_views'] = df['daily_views'].interpolate(method='polynomial', order=2)
Ne zaman hangisi?
- Çok az eksik veri (%5'ten az) ve önemsiz → dropna
- Kategorik sütun, eksik = "bilinmiyor" anlamı → fillna('Unknown')
- Sayısal sütun, dağılımı bozmak istemiyorum → fillna(median)
- Zaman serisi, önceki değerle doldur → ffill
- Zaman serisinde gerçekçi tahmin yapmak istiyorum → interpolate
Bir önemli not: fillna(0) çoğu zaman yanlış bir karardır. Çünkü 0 bir değerdir, eksik veriden farklıdır. Mesela bir satış verisinde "satış 0" ile "satış kaydedilmemiş" çok farklı şeyler. İstatistiklerini bozar.
Durum 6: Tip Dönüşümü — astype vs pd.to_numeric / pd.to_datetime
Bu kısımda Netflix projesinde beni en çok zorlayan yere geldim. Tarih sütununu dönüştürürken yaşadığım deneyimi paylaşayım.
astype → en temel tip dönüşümü fonksiyonu.
df['release_year'].astype(str) # int → string
df['release_year'].astype('int32') # int64 → int32 (bellek tasarrufu)
Ama astype hataya tahammülsüzdür. Eğer sütunda dönüştürülemeyen bir değer varsa, hata fırlatır:
df['rating'].astype(int) # ValueError, çünkü 'PG-13' gibi değerler int değil
İşte burada pd.to_numeric ve pd.to_datetime devreye giriyor. Bunların harika bir parametresi var: errors='coerce'.
# Sayıya çevrilemeyenler NaN olur
s = pd.Series(['1', '2', 'abc', '4'])
result = pd.to_numeric(s, errors='coerce')
# [1.0, 2.0, NaN, 4.0]
# Tarihe çevrilemeyenler NaT (Not a Time) olur
df['date_added'] = pd.to_datetime(df['date_added'], errors='coerce')
errors='coerce', "dönüştüremediğin değerleri NaN/NaT yap" demek. Veri temizlerken çok işe yarayan bir parametre.
Netflix verisinde date_added sütunu "Aug 21, 2023" gibi bir string formatındaydı. Gerçek Netflix kataloğunda bu sütunda eksik satırlar olabilir, bu yüzden errors='coerce' parametresi güvenli bir kullanım sağlar:
df['date_added'] = pd.to_datetime(df['date_added'], errors='coerce')
df['year_added'] = df['date_added'].dt.year
astype'ı errors='coerce' olmadan kullansaydım, eksik bir satırda program çökerdi.
Ne zaman hangisi?
- Veri temizse ve dönüşüm kesin → astype
- Veri kirli, bazı satırlar dönüşmeyebilir → pd.to_numeric (errors='coerce')
- Bellek optimizasyonu yapacağım (int64 → int8 vs.) → astype
- Tarih dönüşümü yapacağım → Her zaman pd.to_datetime
Durum 7: Filtrelenmiş Veride Değişiklik — .copy( ) Meselesi
Bu senaryo, pandas kullanmaya başladığında er ya da geç seni bulan bir konu.
Senaryo: Netflix verisinden sadece filmleri filtreleyip yeni bir sütun eklemek istiyorum.
movies = df[df['type'] == 'Movie']
movies['duration_minutes'] = movies['duration'].str.extract(r'(\d+)').astype(float)
Bu kod Pandas 2.x'te çalıştırılınca meşhur bir uyarı çıkardı:
SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
İlk gördüğümde "kodum çalışıyor zaten, neyse..." diye geçiştirmiştim. Sonra anladım: pandas, movies değişkeninin orijinal df'in bir kopyası mı yoksa görüntüsü (view) mu olduğundan emin değildi.
Pandas 3.0'da büyük değişiklik: Copy-on-Write (CoW) artık varsayılan davranış. Yani:
- Filtreleme yaptığında dönen DataFrame her zaman kopya gibi davranır
- Yeni sütun eklediğinde orijinal df etkilenmez
- Tipik SettingWithCopyWarning artık çıkmıyor
Ben yukarıdaki kodu Pandas 3.0'da çalıştırdım — uyarı yok, ama bir konu var: movies üzerine sütun eklediğimde orijinal df'e bu sütun yansımıyor.
Pratik tavsiye: Hangi versiyon olursa olsun, kafanı karıştırmamak için iyi alışkanlık:
# Bağımsız bir kopya istiyorsan
movies = df[df['type'] == 'Movie'].copy()
movies['duration_minutes'] = movies['duration'].str.extract(r'(\d+)').astype(float)
# Orijinal df'i güncellemek istiyorsan
df.loc[df['type'] == 'Movie', 'duration_minutes'] = (
df.loc[df['type'] == 'Movie', 'duration'].str.extract(r'(\d+)').astype(float)
)
.copy( ) ile "bu artık bağımsız bir DataFrame" demiş olursun. .loc ile atama yaparsan orijinal DataFrame'i güncellersin. İkisi farklı niyetler, ikisini de açıkça yazmak kodu okuyan kişi (gelecekteki sen dahil!) için daha iyi.
Tüm Karşılaştırmalar
| Senaryo | Fonksiyon | Ne zaman |
|--------------------|--------------------------------|-------------------------------------|
| Veri seçimi | iloc | Pozisyonla |
| Veri seçimi | loc | Etiketle (sütun adı, koşul) |
| Birleştirme | concat | Yapıştırma (alt alta, yan yana) |
| Birleştirme | merge | Sütun üzerinden (SQL JOIN gibi) |
| Birleştirme | join | Index üzerinden |
| Dönüşüm | Series.map | Tek sütun + sözlük/fonksiyon |
| Dönüşüm | apply | Karmaşık ve esnek |
| Dönüşüm | DataFrame.map | Hücre hücre işlem (eski applymap) |
| Gruplama sonrası | agg | Grup başı bir özet |
| Gruplama sonrası | transform | Grup başı bir değer, satır aynı |
| Gruplama sonrası | apply | Karmaşık grup operasyonları |
| Eksik veri | dropna | Sil |
| Eksik veri | fillna | Belirli değerle doldur |
| Eksik veri | ffill / bfill | Önceki/sonraki değerle doldur |
| Eksik veri | interpolate | Tahmin et (zaman serisi) |
| Tip dönüşümü | astype | Veri temiz, dönüşüm kesin |
| Tip dönüşümü | pd.to_numeric / pd.to_datetime | Veri kirli, errors='coerce' |
| Filtre + atama | .copy() | Bağımsız çalışma |
| Filtre + atama | .loc[mask, col] | Orijinali güncelleme |
Pandas Versiyonu Uyarısı
Bu yazıyı yazarken Pandas 3.0'ı temel aldım. Eğer hâlâ daha eski bir versiyon kullanıyorsan dikkat etmen gereken birkaç şey:
| Konu | Pandas 2.x | Pandas 3.0 |
|----------------------------|-------------------------------|---------------------------------|
| Hücre hücre dönüşüm | df.applymap(fn) | df.map(fn) |
| fillna ile yön | df.fillna(method='ffill') | df.ffill() |
| Filtre + atama uyarısı | SettingWithCopyWarning çıkar | Copy-on-Write ile çıkmaz |
Versiyonunuzu öğrenmek için: pd.__version__
Kapanış
Pandas'ta aynı işi yapan birden fazla yol olması bazen iyi, bazen kafa karıştırıcı. Ama her birinin bir varlık sebebi var biri performans için, biri okunabilirlik için, biri farklı senaryolara uygun.
Benim deneyimim şu: bir fonksiyonun ne zaman kullanılacağını öğrenmek için onunla bir hata yapmak gerekiyor. SettingWithCopyWarning'ı görmeden .copy()'nin önemini anlamadım. astype koduma hata fırlatmadan pd.to_numeric'in errors='coerce' parametresinin değerini bilmiyordum. Yani olay tamamen pratikte. Prensibim: pratik yap, yanlış yap, öğren.
Bu yüzden, eğer pandas öğreniyorsan, gerçek veriyle çalış. Sentetik veri çok temiz olduğu için bu hataları yaşamazsın. Kaggle'dan (bilmiyorsan mutlaka bak), devlet açık veri portallarından, herhangi bir gerçek CSV bul ve içine dal.
Bu yazıdaki örneklerin tamamı GitHub'daki Netflix Catalog Analysis projemden. Kodu görmek, çalıştırmak ya da kendi versiyonunu yapmak istersen oraya bakabilirsin. Şimdiden kolay gelsin.
Veri analizi yolculuğumda öğrendiklerimi paylaşmak amacıyla bu içeriği hazırladım İlk teknik içeriğim olduğu için hatalarım veya atladığım yerler olabilir — geri bildirimlerinize açığım, yorumlarınızı bekliyorum.
Nisa Kaya — Bilgisayar Mühendisliği 3. sınıf öğrencisi.
GitHub: github.com/nisakayaa
