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.

ℹ NOT

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:

// ERK TERMINAL python
# 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:

// ERK TERMINAL python
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:

// ERK TERMINAL bash
# 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.

// ERK TERMINAL python
# İ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.

// ERK TERMINAL python
# 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.

// ERK TERMINAL python
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:

// ERK TERMINAL python
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:

// ERK TERMINAL python
df['title_length'] = df['title'].apply(len)

DataFrame üzerinde — satır satır veya sütun sütun işlem yapar:

// ERK TERMINAL python
# 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.

// ERK TERMINAL bash
# Tüm string sütunları küçük harfe çevir
df_strings = df[['type', 'rating']].map(lambda x: str(x).lower())
ℹ Önemli Not

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:

// ERK TERMINAL python
# 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:

// ERK TERMINAL python
df.groupby('rating')['release_year'].agg('mean')

Sonuç şuna benzer çıkar (gerçek sayılar verinize göre değişir):

// ERK TERMINAL python
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:

// ERK TERMINAL python
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:

// ERK TERMINAL python
# 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.

// ERK TERMINAL python
# 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.

// ERK TERMINAL python
# 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).

// ERK TERMINAL python
# Ö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()
ℹ NOT

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.

// ERK TERMINAL python
# 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.

// ERK TERMINAL python
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:

// ERK TERMINAL python
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'.

// ERK TERMINAL python
# 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:

// ERK TERMINAL python
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.

// ERK TERMINAL python
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ı:

// ERK TERMINAL python
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:

// ERK TERMINAL python
# 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

// ERK TERMINAL bash
| 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:

// ERK TERMINAL python
| 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.


https://github.com/nisakayaa/netflix-data-analysis
https://github.com/nisakayaa/netflix-data-analysis

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