Gerçek veri kirlidir
Ilgaz Bank'ın şubelerden toplanan kredi başvuru dosyasında şehir adı bir satırda "ankara", diğerinde " ANKARA " yazar; tutar sütunu "12.500,00 TL" gibi metindir; bazı gelirler boştur; aynı başvuru iki kez girilmiştir. Analizin yüzde yetmişi bu temizliktir. Bu derste pandas'ın temizlik araçlarını bir akış olarak öğreneceksin: eksikleri gör → türleri düzelt → metni standartlaştır → yinelenenleri at → aykırıları işaretle.
Eksik değer: isna, fillna, dropna
pandas eksik değeri NaN (sayısal) ya da NaT (tarih) ile gösterir. isna() maske verir; isna().sum() sütun başına eksik sayısı. Karar iki yönlü: satırı at (dropna) ya da doldur (fillna). Doldurma değeri iş bilgisine bağlıdır: gelir için medyan, şehir için "Bilinmiyor", sayaç için 0.
import pandas as pd, io
csv = """basvuru,musteri,sehir,gelir,skor
1,Ayşe,Ankara,25000,710
2,Mehmet,,18000,
3,Zeynep,İzmir,,655
4,Ali,Bursa,42000,780
5,Can,,,
"""
df = pd.read_csv(io.StringIO(csv))
print(df.isna().sum())
print(df.dropna(subset=["gelir"]).shape) # geliri olmayanı at
print(df.dropna(thresh=4).shape) # en az 4 dolu alanı olanı tut
temiz = df.copy()
temiz["gelir"] = temiz["gelir"].fillna(temiz["gelir"].median())
temiz["sehir"] = temiz["sehir"].fillna("Bilinmiyor")
temiz["skor"] = temiz["skor"].fillna(0)
print(temiz)
df.fillna(0, inplace=True) hâlâ çalışır ama Copy-on-Write ile beklenmedik davranır ve pandas ekibi kaldırmayı planlıyor. Her zaman sonucu ata: df["x"] = df["x"].fillna(0).Tür dönüşümü
CSV'den gelen "12.500,00 TL" metni toplanamaz. Önce metni temizle, sonra pd.to_numeric ile sayıya çevir. errors="coerce" çevrilemeyeni NaN yapar; hata fırlatmak yerine sonradan isna ile yakalarsın. Tarih için pd.to_datetime(..., format=...); biçimi açıkça yazmak hem hız hem güvenlik sağlar. Tam sayı/kategori gibi dönüşümler astype.
import pandas as pd
df = pd.DataFrame({
"tutar": ["12.500,00 TL", "1.250,50 TL", "bilinmiyor", "800,00 TL"],
"tarih": ["05.03.2024", "17.03.2024", "01.04.2024", "hatalı"],
"taksit": ["12", "6", "3", "24"],
})
tmp = (df["tutar"].str.replace(" TL", "", regex=False)
.str.replace(".", "", regex=False)
.str.replace(",", ".", regex=False))
df["tutar_num"] = pd.to_numeric(tmp, errors="coerce")
df["tarih_dt"] = pd.to_datetime(df["tarih"], format="%d.%m.%Y", errors="coerce")
df["taksit"] = df["taksit"].astype(int)
print(df.dtypes)
print(df[["tutar_num", "tarih_dt", "taksit"]])
print("çevrilemeyen tutar:", int(df["tutar_num"].isna().sum()))
Metin temizliği: .str
Bir metin Series'inin .str erişimcisi, Python string metotlarını tüm sütuna vektörleştirilmiş uygular: strip, lower, upper, title, replace, contains, startswith, split, len. Eksik değerler atlanır (NaN kalır).
import pandas as pd
s = pd.Series([" ankara", "ANKARA ", "İzmir", "izmir", "Bursa ", None])
std = s.str.strip().str.title()
print(std.tolist())
print(std.value_counts())
print(s.str.contains("ank", case=False, na=False).tolist())
email = pd.Series(["ayse@anadolu.com", "MEHMET@Gmail.COM", "zeynep@anadolu.com"])
print(email.str.lower().str.split("@").str[1].value_counts())
str.title() "izmir" → "Izmir" yapar (noktalı İ sorunu). Şehir gibi sınırlı listelerde bir eşleme sözlüğü ve .map daha güvenlidir: s.str.strip().str.lower().map(sehir_sozlugu).Yinelenen kayıtlar
duplicated() ikinci ve sonraki tekrarlar için True verir; subset ile hangi sütunlara göre bakılacağını, keep ile hangisinin tutulacağını ("first", "last", False) seçersin. drop_duplicates aynı parametrelerle siler.
import pandas as pd
df = pd.DataFrame({
"basvuru": [1, 2, 2, 3, 1],
"musteri": ["Ayşe", "Mehmet", "Mehmet", "Zeynep", "Ayşe"],
"skor": [710, 640, 655, 700, 715],
})
print(df.duplicated(subset=["basvuru"]).tolist())
print(df.drop_duplicates(subset=["basvuru"], keep="last").sort_values("basvuru"))
print("tam kopya satır:", int(df.duplicated().sum()))
Aykırı değer: IQR kuralı
Gelir sütununda 25.000'ler arasında bir 2.500.000 görürsen bu hata mı gerçek mi? Önce işaretle, sonra iş birimine sor. Basit ve sağlam yöntem IQR: Q1 ve Q3 çeyrekleri, IQR = Q3 - Q1; [Q1 - 1.5·IQR, Q3 + 1.5·IQR] dışı aykırıdır. clip ile sınırlara çekebilir ya da maskeyle ayırabilirsin.
import pandas as pd
gelir = pd.Series([25000, 18000, 31000, 42000, 27000, 2500000, 22000, 150])
q1, q3 = gelir.quantile(0.25), gelir.quantile(0.75)
iqr = q3 - q1
alt, ust = q1 - 1.5 * iqr, q3 + 1.5 * iqr
aykiri = (gelir < alt) | (gelir > ust)
print(f"sınırlar: {alt:.0f} - {ust:.0f}")
print(gelir[aykiri].tolist())
print(gelir.clip(lower=alt, upper=ust).astype(int).tolist())
Kategorik tür
Şehir, ürün kategorisi, durum gibi az sayıda değer alan metin sütunları astype("category") ile hem bellek hem hız kazandırır. groupby yaparken observed=True vererek yalnızca veride bulunan kategorileri al.
| Sorun | Araç | Not |
|---|---|---|
| Boş hücre | isna, fillna, dropna | Doldurma değeri iş kararıdır |
| Metin sayı | .str.replace + to_numeric(errors="coerce") | Önce binlik/ondalık ayıracı düzelt |
| Metin tarih | to_datetime(format=...) | Biçimi açıkça yaz |
| Kirli metin | .str.strip().str.lower(), .map | Türkçe İ/ı için eşleme |
| Tekrar | duplicated, drop_duplicates | subset ile iş anahtarı |
| Uç değer | quantile, clip | Silme değil işaretle |
Derinleş: Türkçe metin, grup bazlı doldurma, regex ve doğrulama
Temel temizlik akışını (eksik → tür → metin → tekrar → aykırı) gördün. Gerçek projelerde bu akışı bozan birkaç tuzak daha vardır: Türkçe İ/ı harfleri, tutarsız sütun adları, tek bir medyanla doldurmanın yanlış olduğu gruplu veriler, telefon ve adet gibi metne gömülmüş değerler ve temizlikten sonra "gerçekten temiz mi?" sorusu. Bu bölüm bunları kapatıyor.
Türkçe büyük-küçük harf tuzağı
Python'un lower() metodu Unicode kurallarını uygular, Türkçe kurallarını değil: "İ".lower() iki karakterlik "i̇" (i + birleşen nokta) verir, "I".lower() ise "i" verir (Türkçede "ı" olmalıydı). Bu yüzden "İZMİR" küçültülünce "izmir" ile eşleşmez ve şehir sayımları bölünür. Çözüm: küçültmeden önce bu iki harfi elle çevirmek.
import pandas as pd
print("İZMİR".lower() == "izmir", len("İ".lower()))
TR_KUCUK = str.maketrans({"İ": "i", "I": "ı"})
def tr_lower(s: pd.Series) -> pd.Series:
return s.str.strip().str.translate(TR_KUCUK).str.lower()
sehir = pd.Series(["İZMİR", " izmir", "ISPARTA", "Isparta ", "ıspartа"])
print(tr_lower(sehir).tolist())
print(tr_lower(sehir).value_counts().to_dict())
Son elemanı dikkatle incele: "ıspartа" içinde Kiril "а" harfi var ve gözle fark edilmez. Kopyala-yapıştır verilerde böyle hayalet karakterler, sıfır genişlikli boşluk (\u200b) ve bölünmez boşluk (\xa0) sık görülür; s.str.normalize("NFKC") ve s.str.replace("\xa0", " ") ilk savunma hattıdır. Sınırlı değer kümelerinde (şehir, kategori) en güvenlisi bir eşleme sözlüğüyle map edip eşleşmeyenleri raporlamaktır.
Sütun adlarını standartlaştırma
Excel'den gelen başlıklar "Müşteri Adı ", "TUTAR (TL)" gibi olur. Kodda df["müşteri_adı"] yazmak hem yazım hatasına açık hem de klavyeye bağımlıdır. Tüm sütun adlarını bir kerede küçük harf, ASCII ve alt çizgili yapmak kodu sadeleştirir:
import pandas as pd
df = pd.DataFrame(columns=["Müşteri Adı ", "TUTAR (TL)", "Şehir", "Sipariş Tarihi"])
ceviri = str.maketrans("çğıöşüÇĞİÖŞÜ", "cgiosuCGIOSU")
yeni = (df.columns.str.strip()
.str.translate(ceviri)
.str.lower()
.str.replace(r"[^a-z0-9]+", "_", regex=True)
.str.strip("_"))
df.columns = yeni
print(df.columns.tolist())
Grup bazlı ve sıralı doldurma
Tüm sütunun medyanıyla doldurmak, İstanbul'daki eksik geliri Bursa'nın geliriyle karıştırır. Daha dürüst olan, eksiği kendi grubunun medyanıyla doldurmaktır: groupby(...).transform("median") her satıra grubunun değerini taşır, fillna bu Series'i satır satır kullanır. Zaman sıralı verilerde (günlük stok, kur) ise son bilinen değeri ileri taşımak (ffill) ya da iki değer arasını doğrusal tamamlamak (interpolate) daha anlamlıdır.
import pandas as pd
import numpy as np
df = pd.DataFrame({
"sehir": ["Ankara", "Ankara", "Ankara", "Bursa", "Bursa"],
"gelir": [20000, np.nan, 30000, 50000, np.nan],
})
grup_medyani = df.groupby("sehir")["gelir"].transform("median")
df["gelir_dolu"] = df["gelir"].fillna(grup_medyani)
print(df)
stok = pd.Series([100, np.nan, np.nan, 70], index=pd.date_range("2024-03-01", periods=4))
print(stok.ffill().tolist()) # son bilinen değer
print(stok.interpolate().tolist()) # doğrusal: 90, 80
Regex ile değer ayıklama: str.extract
str.extract(desen) her satırda düzenli ifadenin (regular expression) yakalama gruplarını sütun olarak döndürür; eşleşmeyen satır NaN olur. Telefon, plaka, "3 adet x 45,50 TL" gibi metinlerden sayı çekmek için idealdir. Bir telefon numarasını standart biçime getirmek tipik bir örnektir:
import pandas as pd
tel = pd.Series(["0532 111 22 33", "+90 (505) 444-5566", "5321112233", "212 555 00 00", "abc"])
rakam = tel.str.replace(r"\D", "", regex=True) # rakam dışı her şeyi sil
gsm = rakam.str.extract(r"^(?:90|0)?(5\d{9})$")[0] # ülke kodu / 0 isteğe bağlı
print(gsm.tolist())
aciklama = pd.Series(["3 adet x 45,50 TL", "12 adet x 9,90 TL"])
parca = aciklama.str.extract(r"(?P<adet>\d+) adet x (?P<fiyat>[\d,]+)")
parca["fiyat"] = pd.to_numeric(parca["fiyat"].str.replace(",", ".", regex=False))
parca["adet"] = parca["adet"].astype(int)
print(parca.assign(tutar=parca["adet"] * parca["fiyat"]))
Sayıyı aralığa çevirmek: pd.cut ve pd.qcut
Yaş, tutar, skor gibi sürekli değerleri raporlamak için aralıklara (bin) bölersin. pd.cut senin verdiğin sınırları kullanır, aralıklar varsayılan olarak sağdan kapalıdır: (18, 30] 18'i içermez, 30'u içerir. pd.qcut ise veriyi eşit sayıda gözleme bölen çeyrek/dilim sınırlarını kendisi bulur.
| Araç | Ne zaman | Dikkat |
|---|---|---|
replace({"E": "Evet"}) | Birkaç değeri değiştir, kalanı aynen kalsın | Sözlükte olmayan değer korunur |
map(sozluk) | Tüm değerleri bir sözlükle çevir | Sözlükte olmayan NaN olur (eşleşmeyeni yakalamak için iyi) |
where / mask | Koşula uymayanı (where) ya da uyanı (mask) değiştir | s.mask(s < 0, 0) negatifleri 0 yapar |
pd.cut | Sabit sınırlarla aralık | Sağdan kapalı; right=False ile değiştir |
pd.qcut | Eşit büyüklükte dilimler | Çok tekrar eden değerde duplicates="drop" |
import pandas as pd yas = pd.Series([17, 18, 25, 30, 31, 64]) grup = pd.cut(yas, bins=[0, 18, 30, 120], labels=["genç", "yetişkin", "olgun"]) print(grup.tolist()) print(grup.value_counts(sort=False).to_dict()) tutar = pd.Series([50, 120, 80, 900, 300, 60, 2000, 75]) print(pd.qcut(tutar, q=4, labels=["Ç1", "Ç2", "Ç3", "Ç4"]).tolist()) print(tutar.mask(tutar > 1000, 1000).tolist()) # üst sınır
Temizlik sonrası doğrulama
Temizleme fonksiyonunun sonunda veriye kısa "sözleşme" kontrolleri koymak, kaynak sistem değiştiğinde hatanın rapora sızmasını engeller: zorunlu sütunlar var mı, kimlik sütunu tekil mi, tutar negatif mi, tarih beklenen aralıkta mı? assert ile ya da hatalı satırları ayrı bir "karantina" tablosuna ayırarak yapılır.
Dört küçük araç: select_dtypes, explode, query, pipe
import pandas as pd
d = pd.DataFrame({"no": [1, 2], "sehir": ["Van", "Kars"], "urunler": [["çay", "kek"], ["kahve"]], "tutar": [40.0, 25.0]})
print(d.select_dtypes(include="number").columns.tolist())
print(d.explode("urunler"))
print(d.query("tutar > 30")["sehir"].tolist())
print(d.pipe(lambda x: x.assign(kdvli=x["tutar"] * 1.2))["kdvli"].round(1).tolist())
select_dtypes keşfin ilk adımıdır: hangi sütunlar sayı, hangileri metin? Sayı olması gereken sütun metin çıkıyorsa temizlik başlar. explode API'den gelen liste sütununu satırlara açar; diğer sütunlar tekrar eder, toplam alırken dikkat et. query uzun filtreleri okunur yapar, dış değişkeni @ ile alır. pipe kendi fonksiyonunu zincire sokar.
Derinleş: gerçek kirli veriyle temizlik
Derste temizlik tekniklerini küçük, elle kurulmuş tablolarla gördün. Şimdi aynı teknikleri gerçek dağınıklıkta deneyelim. İki kaynak kullanacağız: kirli_veri seti (Kesit Power BI'daki Power Query derslerinin kaynak dosyaları: elle doldurulmuş Excel sayfaları ve CRM dökümü) ve anadolu_market setindeki raw_signups tablosu (web formundan gelen 52 ham kayıt). Power Query'de tıklayarak yaptığın her adımın pandas karşılığını burada kodla kuracaksın.
| Tablo | Sorunlar | pandas çaresi |
|---|---|---|
kirli_veri/bayi | Üstte rapor başlığı, boş satır, yinelenen satır, " istanbul " gibi boşluklu şehir, "1.250,50" ve "-" tutarları, "bilinmiyor" tarihi | skiprows, dropna(how="all"), drop_duplicates, .str.strip, to_numeric(errors="coerce") |
kirli_veri/hedef | Aylar ayrı sütunlarda (geniş tablo) | melt |
kirli_veri/urun | Kategori yalnızca grubun ilk satırında; kod ve ad tek hücrede | ffill, str.split(n=1, expand=True) |
kirli_veri/musteri | İkinci satır asıl başlık, e-posta harf karmaşası, yinelenen numara, 30 Şubat | skiprows=1, str.lower, errors="coerce" |
anadolu_market/raw_signups | Aynı kişi iki kez, karışık harfli e-posta, "ISTANBUL" / "istanbul" / "İstanbul", iki tarih biçimi | Normalleştirilmiş anahtarla tekilleştirme, iki geçişli tarih çevirme |
Önce bak, sonra temizle
Temizliğe başlamadan ham tabloya bak. bayi dosyası Excel'den geldiği için ilk satır Column1..Column5 gibi anlamsız adlar, ikinci satır rapor başlığı, asıl başlık üçüncü satırda. skiprows=2 ilk iki satırı atlar ve üçüncüyü başlık yapar.
import veri
ham = veri.oku("kirli_veri", "bayi")
print(ham.head(3))
df = veri.oku("kirli_veri", "bayi", skiprows=2)
print(df.columns.tolist())
print("tamamen boş satır:", int(df.isna().all(axis=1).sum()))
print("yinelenen satır:", int(df.duplicated().sum()))
Türkçe sayı ve Türkçe büyük harf
"1.250,50" metninde nokta binlik, virgül ondalıktır. Önce noktaları sil, sonra virgülü noktaya çevir; "-" gibi sayı olmayan değerleri to_numeric(errors="coerce") eksik yapar. Şehirlerde ise iki tuzak var: baş/son boşluk ve Türkçe I/İ. "ISTANBUL".lower() "istanbul" verir ama "İstanbul".lower() noktalı bir birleşik karakter üretir; ikisi eşleşmez. Pratik çözüm, karşılaştırma için ASCII'ye katlanmış bir eşleme anahtarı (matching key) üretmek ve görüntülenecek adı bir sözlükten almaktır.
import veri
import pandas as pd
OZEL = {"istanbul": "İstanbul", "izmir": "İzmir"}
def sehir_std(s):
anahtar = (s.str.strip().str.replace("İ", "i").str.replace("I", "i").str.lower())
return anahtar.map(lambda x: OZEL.get(x, x.capitalize()) if isinstance(x, str) else x)
df = veri.oku("kirli_veri", "bayi", skiprows=2).dropna(how="all").drop_duplicates()
df["Şehir"] = sehir_std(df["Şehir"])
metin = df["Tutar"].str.replace(".", "", regex=False).str.replace(",", ".", regex=False)
df["Tutar"] = pd.to_numeric(metin, errors="coerce")
print(df[["Bayi", "Şehir", "Tutar"]])
print("Toplam:", df["Tutar"].sum())
İki tarih biçimi bir arada
raw_signups tablosunda tarihlerin bir kısmı "29.07.2024", bir kısmı "2024-02-10" biçiminde. Tek bir format ile çevirirsen diğer biçim NaT olur. Güvenli yol iki geçiştir: önce birinci biçimi dene, kalan boşlukları ikinci biçimle doldur. Her geçişte format açıkça verildiği için gün ile ay asla yer değiştirmez.
import veri
import pandas as pd
rs = veri.oku("anadolu_market", "raw_signups")
t = rs["signup_date"].str.strip()
tarih = pd.to_datetime(t, format="%d.%m.%Y", errors="coerce")
tarih = tarih.fillna(pd.to_datetime(t, format="%Y-%m-%d", errors="coerce"))
print("çevrilemeyen:", int(tarih.isna().sum()))
rs["email"] = rs["email"].str.strip().str.lower()
print("satır:", len(rs), "| farklı id:", rs["id"].nunique(), "| farklı e-posta:", rs["email"].nunique())
id 40 farklı değer gösteriyor ama normalleştirilmiş e-posta 39. Çünkü "can.kaya@gmail.com" iki farklı id ile iki kez kayıt olmuş. Tekilleştirme anahtarını iş kuralı belirler: "aynı e-posta aynı kişidir" kuralını iş birimiyle teyit et ve raporuna yaz.Geniş tabloyu uzatmak ve gruplu listeyi doldurmak
Finans ekibinin hedef dosyası her ayı ayrı sütunda tutar. Grafikte ay eksenine koymak ya da satışlarla birleştirmek için uzun biçim gerekir: melt. Satın alma listesinde kategori yalnızca grubun ilk satırında yazılıdır; ffill bir üstteki değeri aşağı taşır.
import veri
hedef = veri.oku("kirli_veri", "hedef")
uzun = hedef.melt(id_vars="Kategori", var_name="Ay", value_name="Hedef")
print(uzun.head(5))
urun = veri.oku("kirli_veri", "urun")
urun["Kategori"] = urun["Kategori"].ffill()
print(urun.head(4))
bayi_temizle, signup_temizle) ve sonunda bir kalite raporu üret: satır sayısı, tam tekrar, anahtar tekrarı, sütun başına eksik. Bu rapor her çalıştırmada aynı biçimde çıktığında, kaynak dosya bozulduğunda bunu ilk sen fark edersin.Temizlik fonksiyonu kalıbı
Üretimde temizlik adımlarını def temizle(df) -> pd.DataFrame biçiminde, girdiyi değiştirmeyen, zincirlenebilir fonksiyonlara böl ve df.pipe(temizle_sehir).pipe(tutar_sayisal) ile bağla. Her fonksiyona küçük bir test yaz: 5 satırlık örnek girdi, beklenen çıktı. Böylece veri kaynağı değiştiğinde hangi adımın kırıldığını saniyede görürsün.
Eksikliğin türü
Eksiklik rastgele mi (sensör arızası) yoksa anlamlı mı (müşteri gelir beyan etmedi)? İkinci durumda doldurmak yerine "beyan_yok" bayrağı eklemek modele ve rapora daha dürüst bilgi verir. Eksik oranı yüzde 40'ı aşan sütunu doldurmak yerine çoğunlukla atmak doğrudur.
Nullable türler
Tam sayı sütununda NaN olunca pandas sütunu float yapar. astype("Int64") (büyük I) eksik destekleyen tam sayı türüdür; pandas 3 ile metinler için varsayılan str türü de gelir. Üretim kodunda tür kontrolünü dtype adına değil davranışa göre yap (pd.api.types.is_numeric_dtype).