Ana içeriğe geç
Kesit Python
Orta · Veri analizine giriş

Veri temizleme: eksik, tür, metin, yinelenen

isna/fillna/dropna ile eksik değer, to_numeric/to_datetime/astype ile tür dönüşümü, .str metotları, duplicated, IQR ile aykırı değer ve category türü.

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

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)
inplace kullanmadf.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())
title ve Türkçestr.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.

SorunAraçNot
Boş hücreisna, fillna, dropnaDoldurma değeri iş kararıdır
Metin sayı.str.replace + to_numeric(errors="coerce")Önce binlik/ondalık ayıracı düzelt
Metin tarihto_datetime(format=...)Biçimi açıkça yaz
Kirli metin.str.strip().str.lower(), .mapTürkçe İ/ı için eşleme
Tekrarduplicated, drop_duplicatessubset ile iş anahtarı
Uç değerquantile, clipSilme değil işaretle
Derinleş

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 zamanDikkat
replace({"E": "Evet"})Birkaç değeri değiştir, kalanı aynen kalsınSözlükte olmayan değer korunur
map(sozluk)Tüm değerleri bir sözlükle çevirSözlükte olmayan NaN olur (eşleşmeyeni yakalamak için iyi)
where / maskKoşula uymayanı (where) ya da uyanı (mask) değiştirs.mask(s < 0, 0) negatifleri 0 yapar
pd.cutSabit sınırlarla aralıkSağdan kapalı; right=False ile değiştir
pd.qcutEş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.

Temizliği belgeleHer doldurma, silme ve kırpma bir iş kararıdır. Kaç satırın atıldığını, kaç değerin doldurulduğunu raporun altına yaz; aksi halde iki ekip aynı veriden farklı rakam bulur ve hangisinin doğru olduğu tartışılır.
Gerçek işteBankacılık ve e-ticaret ekiplerinde temizlik kodu bir kere yazılıp her gün çalışır. Bu yüzden şehir eşleme sözlüğü, sütun adı standardı ve doğrulama kuralları çoğunlukla ayrı bir modülde ya da yapılandırma dosyasında tutulur; yeni bir şehir yazımı geldiğinde kod değil sözlük güncellenir. Eşleşmeyen değerleri sessizce "Bilinmiyor" yapmak yerine günlük bir "eşleşmeyenler" listesiyle veri sahibine bildirmek kaliteyi kalıcı olarak artırır.
Derinleş

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ş

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.

TabloSorunlarpandas ç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" tarihiskiprows, dropna(how="all"), drop_duplicates, .str.strip, to_numeric(errors="coerce")
kirli_veri/hedefAylar ayrı sütunlarda (geniş tablo)melt
kirli_veri/urunKategori yalnızca grubun ilk satırında; kod ve ad tek hücredeffill, 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 Şubatskiprows=1, str.lower, errors="coerce"
anadolu_market/raw_signupsAynı kişi iki kez, karışık harfli e-posta, "ISTANBUL" / "istanbul" / "İstanbul", iki tarih biçimiNormalleş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())
Hangi anahtarla tekilleştiriyorsun?Formdaki 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))
Gerçek işteTemizlik adımlarını tek seferlik hücrelere dağıtma; her kaynak için bir fonksiyon yaz (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.
Uzmanlaş

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).

Alıştırmalara geç (31)