Kategori: Hafta Sonu Projeleri

  • Banka Kampanyası Verisi: Kim Vadeli Mevduat Açar?

    Banka Kampanyası Verisi: Kim Vadeli Mevduat Açar?

    Bir banka, müşterilerini telefonla arayıp vadeli mevduat hesabı açmalarını teklif ediyor. On kişiyi arasın, dokuzu “hayır” diyor. Peki kimi aramalı? Bu hafta sonu projesinde gerçek bir banka kampanyasının verisiyle bu soruya cevap arıyoruz.

    Yol boyunca veri biliminin üç klasik tuzağına da basacağız: gizli eksik değerler, yanıltıcı bir başarı ölçüsü ve modelin gelecekten haber alması.

    Tüm kodlar Google Colab’da çalışıyor. Colab tarayıcıda açılıyor ve kodu Google’ın sunucularında çalıştırıyor; bu projede kullandığımız pandas, scikit-learn ve matplotlib orada hazır geliyor. Yani bilgisayarına hiçbir şey kurman gerekmiyor, bir Google hesabı yeterli: Colab’da aç

    Veri seti

    Veri, bir Portekiz bankasının 2008-2010 yılları arasındaki telefonla pazarlama kampanyalarından geliyor ve UCI Machine Learning Repository’de CC BY 4.0 lisansıyla paylaşılıyor. Her satır anonim bir müşteriyle yapılan bir görüşme; isim ya da kimlik bilgisi yok.

    Sütunlar dört gruba ayrılıyor:

    • Müşteri: yaş, meslek, medeni durum, eğitim, kredi durumu
    • Bu kampanyadaki görüşme: ay, gün, iletişim türü, görüşme süresi, kaçıncı arama olduğu
    • Geçmiş: önceki kampanyada aranıp aranmadığı ve sonucu
    • Ekonomi: istihdam, enflasyon endeksi, faiz gibi o döneme ait göstergeler

    Hedef sütun y: müşteri vadeli mevduat açtı mı (yes), açmadı mı (no)?

    Veriyi indirmek

    UCI dosyayı iç içe iki zip olarak veriyor. Diske tek tek açmak yerine bellekte açıyoruz:

    import io
    import os
    import zipfile
    import numpy as np
    import pandas as pd
    import requests
    DOSYA = "bank-additional-full.csv"
    URL = "https://archive.ics.uci.edu/static/public/222/bank+marketing.zip"
    def veriyi_indir():
    dis = zipfile.ZipFile(io.BytesIO(requests.get(URL, timeout=60).content))
    ic_adi = [a for a in dis.namelist() if a.endswith("bank-additional.zip")][0]
    ic = zipfile.ZipFile(io.BytesIO(dis.read(ic_adi)))
    csv_adi = [a for a in ic.namelist() if a.endswith(DOSYA)][0]
    with open(DOSYA, "wb") as f:
    f.write(ic.read(csv_adi))
    if not os.path.exists(DOSYA):
    veriyi_indir()
    df = pd.read_csv(DOSYA, sep=";")
    print(df.shape)
    (41188, 21)

    sep=";" kısmına dikkat et. Bu dosyada ayırıcı virgül değil, noktalı virgül. Vermezsen pandas her satırı tek bir uzun sütun sanır.

    İlk bakış: on kişiden biri

    print("Tekrarlanan satır:", df.duplicated().sum())
    df = df.drop_duplicates().reset_index(drop=True)
    df["hedef"] = (df["y"] == "yes").astype(int)
    print(f"Vadeli mevduat açanların oranı: %{df['hedef'].mean() * 100:.1f}")
    Tekrarlanan satır: 12
    Vadeli mevduat açanların oranı: %11.3

    12 tekrar eden satırı sildikten sonra elimizde 41.176 görüşme kalıyor ve bunların yalnızca yüzde 11,3’ü “evet” ile bitmiş. Bir sınıfın diğerinden çok daha az olduğu bu duruma dengesiz sınıf deniyor. Aklında tut, birazdan canımızı sıkacak.

    Gizli eksikler: unknown ve 999

    df.isna().sum() bu veride her sütun için sıfır gösterir. Ama bu, eksik veri olmadığı anlamına gelmiyor. Eksikler iki kılıkta saklanıyor:

    • Kategorik sütunlarda "unknown" yazısı. En çok default sütununda (kredi borcunu ödememiş mi?): 8.596 satır, yani neredeyse her beş kişiden biri.
    • pdays sütununda 999 değeri. Müşterinin daha önce hiç aranmadığı anlamına geliyor ve satırların yüzde 96’sında bu değer var.

    Bu satırları silmiyoruz. “Bu soruya cevap vermemiş olmak” da bir bilgi; unknown kendi başına bir kategori olarak kalıyor. 999’u ise ortalamaya katılırsa sonucu saçma bir yere çekecek sıradan bir sayı gibi değil, “hiç” anlamında okuyoruz.

    Yeni bir veri setine başlarken isna() ile yetinme. Her kategorik sütunun value_counts() çıktısına bir kez göz at; eksikler genelde orada saklanır.

    Keşif: hangi grupta evet oranı yüksek?

    Aynı hesabı birçok sütun için yapacağımızdan küçük bir fonksiyon yazıyoruz:

    def oran_tablosu(sutun, sira=None):
    t = df.groupby(sutun, observed=True)["hedef"].agg(gorusme="size", evet_orani="mean")
    if sira is not None:
    t = t.reindex(sira)
    t["evet_orani"] = (t["evet_orani"] * 100).round(1)
    return t
    oran_tablosu("poutcome")
    Önceki kampanyanın sonucuGörüşmeEvet oranı
    Başarılı (evet demiş)1.373%65,1
    Başarısız4.252%14,2
    Daha önce aranmamış35.551%8,8

    Geçen sefer “evet” diyen müşterilerin üçte ikisi bu sefer de evet demiş. Tek bir sütun, ortalamanın neredeyse altı katı bir oran gösteriyor.

    Aynı fonksiyonla baktığımız diğer gruplardan öne çıkanlar:

    • İletişim türü: Cep telefonundan aranan müşterilerde oran %14,7, sabit hattan arananlarda %5,2.
    • Yaş: 66 yaş ve üstünde %46,9, 17-25 arasında %21,0. Aradaki yaş grupları %8-15 arasında kalıyor. Ama 66+ grubunda sadece 618 görüşme var.
    • Arama sayısı: İlk aramada oran %13,0; altı ve daha fazla aranmış kişilerde %5,5. Aynı müşteriyi ısrarla aramak işe yaramıyor gibi.

    Ay tablosu ve korelasyon tuzağı

    AYLAR = ["mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]
    oran_tablosu("month", AYLAR)

    Mart, eylül, ekim ve aralıkta evet oranı %44 ile %51 arasında. Mayıs’ta ise %6,4. “Demek ki martı beklemek lazım” demek çok cazip.

    Ama görüşme sayılarına bak: mayısta 13.767 görüşme var, aralıkta 182. Az aranan aylarda banka belki de zaten ilgili olduğunu bildiği müşterileri aradı. Bu tablo ayın sonucu değiştirdiğini göstermiyor; sadece ikisinin birlikte hareket ettiğini gösteriyor. Korelasyon, nedensellik değildir.

    duration tuzağı: veri sızıntısı

    duration, görüşmenin saniye cinsinden süresi:

    df.groupby("y")["duration"].median()
    y
    no 164.0
    yes 449.0

    Evet diyenlerle görüşme, ortanca değere göre neredeyse üç kat uzun sürmüş. Doğal: ikna olan müşteriyle hesap açmanın detaylarını konuşuyorsun. Bu sütun modele girerse sonucu çok iyi tahmin eder.

    Sorun şu: bankanın asıl sorusu “kimi arayalım?” ve bu soru arama yapılmadan önce soruluyor. O anda görüşmenin ne kadar süreceğini bilemezsin. Tahmin anında elinde olmayacak bir bilgiyi modele vermeye veri sızıntısı (data leakage) deniyor. Veri setinin kendi açıklaması da gerçekçi bir model için bu sütunun çıkarılmasını öneriyor. Biz de çıkarıyoruz.

    Model: lojistik regresyon

    from sklearn.compose import ColumnTransformer
    from sklearn.linear_model import LogisticRegression
    from sklearn.model_selection import train_test_split
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import OneHotEncoder, StandardScaler
    CIKAR = ["y", "hedef", "duration", "yas_grubu", "arama_sayisi"]
    X = df.drop(columns=CIKAR)
    y = df["hedef"]
    X_egitim, X_test, y_egitim, y_test = train_test_split(
    X, y, test_size=0.25, stratify=y, random_state=42)
    kategorik = X.select_dtypes(exclude="number").columns.tolist()
    sayisal = X.select_dtypes(include="number").columns.tolist()
    def model_kur(kategorik, sayisal):
    hazirlik = ColumnTransformer([
    ("kategorik", OneHotEncoder(handle_unknown="ignore"), kategorik),
    ("sayisal", StandardScaler(), sayisal),
    ])
    return Pipeline([
    ("hazirlik", hazirlik),
    ("model", LogisticRegression(max_iter=2000, class_weight="balanced")),
    ])
    model = model_kur(kategorik, sayisal)
    model.fit(X_egitim, y_egitim)
    olasilik = model.predict_proba(X_test)[:, 1]
    tahmin = (olasilik >= 0.5).astype(int)

    (yas_grubu ve arama_sayisi, keşif bölümünde tablolar için eklediğimiz yardımcı sütunlar; not defterinde oluşturuluyorlar.)

    Satır satır ne yaptık:

    • stratify=y: Eğitim ve test setinde evet oranı aynı kalsın. Dengesiz veride bunu unutursan test setine şans eseri çok az “evet” düşebilir.
    • OneHotEncoder: “meslek = öğrenci” gibi metin değerlerini 0-1 sütunlarına çevirir. Model sadece sayılarla çalışır.
    • StandardScaler: Yaş (17-98) ile faiz oranı (0-5) gibi farklı ölçekteki sayıları aynı ölçeğe getirir.
    • class_weight="balanced": Az olan “evet” sınıfındaki hataları daha ağır cezalandır. Bunu yazmazsan model, hemen herkese “hayır” demenin kolay yolunu seçer.
    • Pipeline: Hazırlık ve modeli tek bir nesnede toplar. Böylece test verisine eğitimde öğrenilen dönüşümün aynısı uygulanır.

    Accuracy neden yanıltır?

    Hiçbir şey öğrenmemiş, herkese “hayır” diyen bir model düşün:

    from sklearn.metrics import accuracy_score
    herkese_hayir = np.zeros(len(y_test), dtype=int)
    print(f"Herkese 'hayır' diyen model accuracy: %{accuracy_score(y_test, herkese_hayir) * 100:.1f}")
    print(f"Bizim modelin accuracy'si: %{accuracy_score(y_test, tahmin) * 100:.1f}")
    Herkese 'hayır' diyen model accuracy: %88.7
    Bizim modelin accuracy'si: %83.3

    Hiçbir şey öğrenmemiş model daha “başarılı” görünüyor. Oysa bankaya tek bir müşteri kazandırmaz. Dengesiz veride accuracy, çoğunluk sınıfını ne kadar iyi tahmin ettiğini ölçer; bizi ilgilendiren azınlık sınıfı ise kaybolur.

    Bu yüzden başka ölçülere bakıyoruz. Test setindeki 10.294 görüşmede bizim model:

    Sayı
    Doğru yakalanan “evet”742
    Kaçırılan “evet”418
    Boşa aranan (evet tahmin edildi, hayır dedi)1.300
    Doğru “hayır”7.834
    • Recall (duyarlılık): Gerçekten evet diyenlerin yüzde kaçını yakaladık? 742 / 1.160 = %64
    • Precision (kesinlik): Evet dediğimiz kişilerin yüzde kaçı gerçekten evet dedi? 742 / 2.042 = %36
    • ROC-AUC: Model, rastgele seçilmiş bir “evet”i rastgele seçilmiş bir “hayır”ın önüne ne sıklıkla koyuyor? 0,5 yazı tura, 1 kusursuz. Bizim model: 0,80

    Precision’ın %36 olması kötü gibi görünebilir. Ama rastgele arasan bu oran %11 olurdu; model aynı emekle üç kattan fazla müşteri buluyor.

    Asıl soru: kimi önce aramalı?

    Banka herkesi arayamaz. Modelin verdiği olasılığa göre müşterileri sıralayıp onluk dilimlere ayıralım. 1. dilim, modelin en umutlu bulduğu yüzde 10.

    sirali = pd.DataFrame({"gercek": y_test.to_numpy(), "olasilik": olasilik})
    sirali = sirali.sort_values("olasilik", ascending=False).reset_index(drop=True)
    sirali["dilim"] = sirali.index * 10 // len(sirali) + 1
    dilimler = sirali.groupby("dilim")["gercek"].agg(kisi="size", evet="sum", evet_orani="mean")
    Modelin sıralamasına göre 10 müşteri diliminde evet oranı. 1. dilimde %50, 2. dilimde %23; geri kalan dilimler %3 ile %10 arasında. Rastgele aramada oran %11.

    Modelin en umutlu bulduğu yüzde 10’luk dilimde 1.030 kişiden 510’u evet demiş: yaklaşık her iki kişiden biri. Bu dilim, test setindeki tüm evetlerin yüzde 44’ünü tek başına yakalıyor. Yani banka, listenin sadece onda birini arayarak müşterilerin neredeyse yarısına ulaşabilir.

    Bir model, her şeyi doğru tahmin etmek zorunda değil. Bazen tek işi doğru sıralamaktır.

    Sızıntılı model ne kadar iyi görünürdü?

    Aynı modeli bir de duration dahil kurduğumuzda (not defterinin 10. bölümü):

    duration olmadan ROC-AUC: 0.802
    duration ile ROC-AUC: 0.940

    0,94 çok daha parlak duruyor ve bir sunumda insanları kolayca etkiler. Ama gerçek hayatta kullanılamaz. Bir model fazla iyi çıktığında ilk soracağın soru şu olmalı: Bu bilgi, tahmin anında elimde olacak mı?

    Bu projeden kalanlar

    • Eksik veri her zaman NaN olarak gelmez; unknown ve 999 gibi kılıklara bak.
    • Grup oranlarına bakarken görüşme sayısını da yaz. Küçük gruplardaki uç oranlar akıl karıştırır.
    • Dengesiz veride accuracy’ye güvenme; recall, precision ve ROC-AUC’ye bak.
    • Modeli işin sorusuna göre değerlendir. Burada soru “kimi önce arayalım?” olduğu için sıralama kalitesi asıl ölçü.
    • “Fazla iyi” sonuçlarda veri sızıntısından şüphelen.

    Kendin dene

    1. class_weight="balanced" parametresini sil. Recall ve precision nasıl değişti?
    2. Eşiği 0,5 yerine 0,3 ve 0,7 yap. Yakalanan ve boşa aranan kişi sayıları nasıl değişiyor?
    3. LogisticRegression yerine RandomForestClassifier dene. ROC-AUC artıyor mu?
    4. education sütunu için oran_tablosu çalıştır. Ne görüyorsun?

    Kaynak ve lisans

    Moro, S., Cortez, P. ve Rita, P. (2014). A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems, 62, 22-31. Veri: UCI Machine Learning Repository, Bank Marketing, CC BY 4.0.

  • Futbol Veri Projesi 1: Maç Verisini Toplamak ve Temizlemek

    Futbol Veri Projesi 1: Maç Verisini Toplamak ve Temizlemek

    Veri analizi denince akla genelde grafikler ve modeller geliyor. Ama gerçek bir projede zamanın büyük kısmı ondan önceki aşamaya gidiyor: veriyi bulmak, indirmek, tabloya çevirmek ve temizlemek. Bu iki bölümlük hafta sonu projesinde Süper Lig’in 2024-25 sezonunu inceleyeceğiz. Bu ilk bölüm tamamen o görünmeyen ama en önemli işe ayrıldı.

    Sonunda elimizde her satırı bir maç olan, kontrol edilmiş, temiz bir CSV dosyası olacak. Bir de veriye ilk bakış: Ev sahibi olmak gerçekten avantaj mı?

    Bu projeyi seçmemin küçük bir hikâyesi var. Ben Beşiktaşlıyım ve yeni stadımız yapılırken bir dönem kendi stadımız olmadan oynadık. İç saha maçlarını kendi evinde oynayamamak bir takım için ne demek? Ev sahibi olmak gerçekten bir avantaj mı, yoksa sadece taraftarların inandığı bir şey mi? Bu soruların cevabını veride aramak istedim. Bu ilk bölüm o yolculuğun hazırlık kısmı.

    Kodların hepsi Colab’da hazır: Colab’da Aç

    Veri nereden geliyor?

    Veriyi openfootball projesinden alıyoruz. Bu proje, dünyanın birçok liginin maç sonuçlarını gönüllülerin derlediği açık dosyalarda topluyor ve GitHub’da yayımlıyor. Lisansı CC0, yani verinin üzerinde telif hakkı yok; istediğin gibi kullanabilirsin.

    Bir uyarı: Bu resmî bir kaynak değil. Gönüllüler tarafından derlendiği için eksik ya da hatalı kayıtlar olabilir. Zaten bu bölümün amacı da tam olarak bunları bulmak.

    Veri setini seçerken her zaman lisansına bak. “İnternette buldum” demek, onu bir projede ya da yazıda kullanabileceğin anlamına gelmiyor.

    1. Veriyi indirmek

    Veri tek bir JSON dosyasında. Python’un requests kütüphanesiyle indiriyoruz:

    import requests
    URL = "https://raw.githubusercontent.com/openfootball/football.json/master/2024-25/tr.1.json"
    cevap = requests.get(URL, timeout=30)
    cevap.raise_for_status()
    ham = cevap.json()
    print(ham["name"]) # Turkish Süper Lig 2024/25
    print(len(ham["matches"]), "maç") # 342 maç

    raise_for_status() bir güvenlik kemeri: Sunucu bir hata döndürürse (sayfa bulunamadı gibi) program burada durup hatayı söylüyor. Python Dersi 13’teki raise‘in hazır bir örneği.

    Tek bir maç kaydı şöyle görünüyor:

    {
    "round": "Matchday 1",
    "date": "2024-08-09",
    "time": "21:00",
    "team1": "Galatasaray",
    "team2": "Hatayspor",
    "score": {"ht": [0, 0], "ft": [2, 1]}
    }

    team1 ev sahibi, team2 deplasman. ft (full time) maç sonu, ht (half time) ilk yarı skoru. Bu yapı, Python Dersi 8’de öğrendiğimiz sözlük ve listelerin iç içe geçmiş hali.

    2. JSON’dan tabloya

    Pandas’ın json_normalize fonksiyonu iç içe sözlükleri düzleştirip tabloya çeviriyor:

    import pandas as pd
    import numpy as np
    df = pd.json_normalize(ham["matches"])
    print(df.shape) # (342, 8)

    score sözlüğünün içindeki iki alan score.ft ve score.ht adında iki ayrı sütuna dönüştü. Ama her hücrede halâ [2, 1] gibi bir liste var; bunu da ayırmamız gerekecek.

    3. Sütunları düzenlemek

    df = df.rename(columns={
    "round": "hafta", "team1": "ev", "team2": "deplasman",
    "score.ft": "skor", "score.ht": "ilk_yari", "status": "durum",
    })
    df["hafta"] = df["hafta"].str.extract(r"(\d+)").astype(int)
    df["tarih"] = pd.to_datetime(df["date"] + " " + df["time"])
    def ayir(skor, i):
    """[ev, deplasman] listesinden i. elemanı alır; skor yoksa NaN döndürür."""
    return skor[i] if isinstance(skor, list) else np.nan
    df["ev_gol"] = df["skor"].apply(ayir, i=0)
    df["dep_gol"] = df["skor"].apply(ayir, i=1)
    df["iy_ev"] = df["ilk_yari"].apply(ayir, i=0)
    df["iy_dep"] = df["ilk_yari"].apply(ayir, i=1)
    df["hukmen"] = df["durum"].eq("awarded")

    Bu blokta üç önemli temizlik adımı var:

    • “Matchday 1” metninden 1 sayısına: str.extract(r"(\d+)") metnin içindeki rakamları çekiyor. Metin olarak kalırsa “Matchday 10” sıralamada “Matchday 2″den önce gelirdi.
    • Tarih ve saati tek bir zaman sütununa: to_datetime ile gerçek bir tarih tipine çevirince haftanın gününü, ayı ya da iki maç arasındaki gün sayısını kolayca hesaplayabiliyoruz.
    • Skor listelerini ayırmak: ayir fonksiyonu listeden golü alıyor. Skor yoksa hata vermek yerine NaN (boş değer) döndürüyor. Bu yüzden isinstance ile önce gerçekten bir liste olup olmadığına bakıyoruz.

    4. Eksik değerler: verinin anlatmadığı hikaye

    df.isna().sum()
    ev_gol 4
    dep_gol 4
    iy_ev 30
    iy_dep 30

    Veride üç farklı sorun çıktı:

    • Skoru olmayan 4 maç: Hepsi 38. haftada. Kaynak veride bu maçların skoru girilmemiş. Bu maçları analizden çıkaracağız, ama bunu yazıda açıkça söylemek şart: Sonuçlarımız 342 değil, 338 maça dayanacak.
    • İlk yarı skoru olmayan 30 maç: Bu 30 maçın 4’ü yukarıdaki skoru hiç olmayan maçlar. Geri kalan 26 maçta maç sonucu var ama ilk yarı skoru yok. Maç sonucuyla ilgili analizlerde bu bir sorun değil; ilk yarıyla ilgili bir soru sorarsak bu maçları dışarıda bırakmamız gerekecek.
    • Hükmen sonuçlanan 1 maç: 23. haftadaki Galatasaray-Adana Demirspor maçı veride awarded (hükmen) olarak işaretli, skoru 3-0. Bu skor sahada atılan gollerden gelmiyor. Puan tablosunu hesaplarken bu maç sayılmalı, ama “maç başına gol” gibi sahayla ilgili bir istatistiğe katılmamalı.

    Eksik değerlerde verilecek kararların (silmek, doldurmak, ayrı tutmak) tek bir doğru cevabı yok. Doğru cevap sorduğun soruya bağlı. Ama her durumda kural aynı: Ne yaptığını ve neden yaptığını yaz.

    5. Tutarlılık kontrolleri

    Eksik değerler sadece bir tür sorun. Veri “tam” görünüp yanlış da olabilir. Bunu yakalamak için veri hakkında bildiğimiz gerçeklerle karşılaştırıyoruz:

    print("Haftadaki maç sayıları:", df.groupby("hafta").size().unique())
    print("Takım sayısı:", pd.concat([df["ev"], df["deplasman"]]).nunique())
    print("Takım başına maç:", pd.concat([df["ev"], df["deplasman"]]).value_counts().unique())
    print("Aynı ev-deplasman eşleşmesi en fazla:", df.groupby(["ev", "deplasman"]).size().max(), "kez")
    Haftadaki maç sayıları: [9]
    Takım sayısı: 19
    Takım başına maç: [36]
    Aynı ev-deplasman eşleşmesi en fazla: 1 kez

    Hepsi beklendiği gibi: 19 takımlı bir ligde her hafta bir takım bay geçiyor, kalan 18 takım 9 maç yapıyor. Her takım diğer 18 takımla bir iç sahada, bir deplasmanda oynadığı için 36 maç yapıyor. Aynı eşleşme iki kez yazılmamış. Ayrıca takım adları tutarlı: “Fenerbahçe” ile “Fenerbahce” gibi aynı takımın iki farklı yazımı olsaydı, takım sayısı 19’dan fazla çıkardı.

    Bu kontroller sıkıcı görünebilir ama veri analizinin en değerli dakikaları bunlar. Hatalı bir veriyle yapılan en güzel grafik bile yanlış bir şey söyler.

    6. Yeni sütunlar ve temiz dosya

    Skoru olan 338 maçı alıp iki yeni sütun ekliyoruz: toplam gol ve maçın sonucu.

    oynanan = df.dropna(subset=["ev_gol", "dep_gol"]).copy()
    oynanan[["ev_gol", "dep_gol"]] = oynanan[["ev_gol", "dep_gol"]].astype(int)
    oynanan["toplam_gol"] = oynanan["ev_gol"] + oynanan["dep_gol"]
    oynanan["sonuc"] = np.select(
    [oynanan["ev_gol"] > oynanan["dep_gol"], oynanan["ev_gol"] < oynanan["dep_gol"]],
    ["Ev sahibi", "Deplasman"],
    default="Beraberlik",
    )
    oynanan.to_csv("super_lig_2024_25_temiz.csv", index=False)

    np.select bir dizi koşulu sırayla kontrol ediyor: Ev sahibinin golü fazlaysa “Ev sahibi”, deplasmanınki fazlaysa “Deplasman”, hiçbiri değilse “Beraberlik”. Python Dersi 5’teki if-elif-else‘in bütün sütuna tek seferde uygulanan hali.

    Neden temiz veriyi ayrı bir dosyaya kaydediyoruz? Bir sonraki bölümde analize doğrudan bu dosyadan başlayacağız. Temizlik adımlarını her seferinde tekrarlamak hem zaman kaybı hem de hata riski.

    7. İlk bakış: ev sahibi avantajı var mı?

    Temiz veriyle ilk soruyu soralım. Hükmen maçı bu kez dışarıda bırakıyoruz, çünkü sahada oynananlara bakıyoruz:

    sahada = oynanan[~oynanan["hukmen"]]
    print(f"Maç başına gol: {sahada['toplam_gol'].mean():.2f}")
    print(sahada["sonuc"].value_counts(normalize=True).round(3))
    Sahada oynanan maç: 337
    Maç başına gol: 2.93
    Ev sahibi gol ortalaması: 1.66
    Deplasman gol ortalaması: 1.27
    Ev sahibi 0.484
    Deplasman 0.282
    Beraberlik 0.234

    Verideki maçların neredeyse yarısını ev sahibi kazanmış, deplasman ekibi ise dört maçtan sadece birinden biraz fazlasını. Ev sahipleri maç başına deplasman takımlarından yaklaşık 0,4 gol fazla atmış. Bu tek sezonluk bir gözlem; ev sahibi avantajının nedenleri ve takımlar arasındaki farklar, ikinci bölümün konusu.

    Sezonun en golü maçı: 36. haftada Çaykur Rizespor 6-3 Göztepe.

    Bu bölümde ne yaptık?

    • Açık lisanslı bir veri kaynağı bulduk ve lisansını kontrol ettik.
    • JSON’u indirip json_normalize ile tabloya çevirdik.
    • Hafta numarasını, tarihi ve skorları kullanılabilir sütunlara dönüştürdük.
    • Eksik skorları, eksik ilk yarı skorlarını ve hükmen maçı bulup her biri için bir karar verdik.
    • Veriyi bildiğimiz gerçeklerle karşılaştırarak tutarlılığını kontrol ettik.
    • Temiz veriyi kaydedip ilk soruyu sorduk.

    Kendin dene

    1. URL’deki tr.1 yerine en.1 yazarak aynı sezonun Premier Lig verisini indir ve aynı temizliği uygula. Hangi kontroller farklı sonuç veriyor?
    2. İlk yarı skoru olan maçlarda, ilk yarıyı önde kapatan takım maçı kaç kez kazanmış?
    3. Haftalara göre maç başına gol ortalamasını hesapla. Sezon ilerledikçe değişiyor mu?
    4. Ev sahibi kazanma oranı en yüksek olan takım hangisi?

    Sıradaki bölüm

    Futbol Veri Projesi 2’de temiz veriden puan tablosunu hesaplıyoruz, takımların iç saha ve deplasman performansını karşılaştırıyoruz. Bir de önemli bir uyarıyı konuşacağız: Maç sonuçlarından hesaplanan bir puan tablosu, eksik maçlar ve puan silme cezaları gibi nedenlerle resmî tablodan farklı olabilir.

    Devamını oku