Proje tanımı
Ilgaz Bank'ın kart operasyon ekibi senden küçük ama gerçekçi bir araç istiyor: günlük kart işlemlerini okuyacak, kategori bazında harcamayı özetleyecek, şüpheli görünen işlemleri işaretleyecek ve sonucu JSON raporu olarak verecek. Ekip, kuralların test edilmiş olmasını şart koşuyor; çünkü yanlış pozitif bir müşterinin kartını gereksiz yere bloke eder, yanlış negatif ise dolandırıcılığı kaçırır.
Bu ders diğerlerinden farklı: yeni kavram öğretmez, öğrendiklerini birleştirir. Alıştırmalar birbirini tamamlayan altı parçadır; her parçada bir önceki parçanın fonksiyonu hazır gelir. Aşağıda her parçanın kuramı ve örnek çıktısı var; alıştırmalarda kendi sürümünü yazacaksın.
| Parça | Ne yapar | Kullandığın dersler |
|---|---|---|
| 1. Model | Transaction dataclass'ı ve CSV'den okuma | dataclass-typing, json-csv, tarih-saat, decimal |
| 2. Özet | Kategoriye göre toplam | collections, kapsamli-ifadeler |
| 3. Kural A-B | Gece saati ve yüksek tutar | tarih-saat, fonksiyonel |
| 4. Kural C | Aynı kartta 10 dakikada 3 işlem | iteratorler, tarih-saat, sıralama |
| 5. Rapor | JSON çıktı, tip dönüşümü | json-csv, decimal |
| 6. Test | unittest ile kural testleri | test-yazma |
Parça 1: Veri modeli ve CSV okuma
Her işlem beş alan taşır: işlem kimliği, kart numarası (maskeli), zaman, tutar, kategori. Tutar para olduğu için Decimal, zaman datetime. frozen=True ile işlem nesnesi değiştirilemez olur: analiz sırasında kimse bir tutarı yanlışlıkla güncelleyemez. CSV metni csv.DictReader ile okunur, her satır Transaction'a dönüştürülür.
import csv
import io
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
@dataclass(frozen=True)
class Transaction:
id: str
kart: str
zaman: datetime
tutar: Decimal
kategori: str
VERI = """id,kart,zaman,tutar,kategori
T1,****1111,2024-03-15 09:12:00,245.90,market
T2,****1111,2024-03-15 13:40:00,1200.00,elektronik
T3,****2222,2024-03-15 02:15:00,8500.00,kuyumcu
T4,****3333,2024-03-15 23:50:00,60.00,yakit
T5,****3333,2024-03-15 23:54:00,75.50,yakit
T6,****3333,2024-03-15 23:58:00,90.00,market
T7,****2222,2024-03-15 18:00:00,430.00,market
"""
def csv_oku(metin: str) -> list[Transaction]:
islemler = []
for satir in csv.DictReader(io.StringIO(metin)):
islemler.append(Transaction(
id=satir["id"],
kart=satir["kart"],
zaman=datetime.strptime(satir["zaman"], "%Y-%m-%d %H:%M:%S"),
tutar=Decimal(satir["tutar"]),
kategori=satir["kategori"],
))
return islemler
islemler = csv_oku(VERI)
print(len(islemler), "işlem okundu")
print(islemler[2])
print("tutar türü:", type(islemler[0].tutar).__name__, "| saat:", islemler[2].zaman.hour)
Parça 2: Kategoriye göre toplam
Klasik gruplama: sözlükte biriktir. defaultdict(Decimal) başlangıç değeri olarak Decimal(0) verir, böylece += doğrudan çalışır. Çıktıyı toplam tutara göre azalan sıralamak raporun okunurluğunu artırır.
from collections import defaultdict
from decimal import Decimal
# Parça 1'deki islemler listesini yeniden kuralım (kısa sürüm)
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class Transaction:
id: str
kart: str
zaman: datetime
tutar: Decimal
kategori: str
islemler = [
Transaction("T1", "****1111", datetime(2024, 3, 15, 9, 12), Decimal("245.90"), "market"),
Transaction("T2", "****1111", datetime(2024, 3, 15, 13, 40), Decimal("1200.00"), "elektronik"),
Transaction("T3", "****2222", datetime(2024, 3, 15, 2, 15), Decimal("8500.00"), "kuyumcu"),
Transaction("T7", "****2222", datetime(2024, 3, 15, 18, 0), Decimal("430.00"), "market"),
]
def kategori_toplam(islemler: list[Transaction]) -> dict[str, Decimal]:
toplam: dict[str, Decimal] = defaultdict(Decimal)
for t in islemler:
toplam[t.kategori] += t.tutar
return dict(sorted(toplam.items(), key=lambda kv: kv[1], reverse=True))
for kategori, tutar in kategori_toplam(islemler).items():
print(f"{kategori:12} {tutar:>10}")
Parça 3 ve 4: Şüpheli işlem kuralları
Operasyon ekibinin üç kuralı var; bir işlem bunlardan en az birine uyarsa şüpheli sayılır ve hangi kurallara takıldığı raporda görünür:
- gece: işlem saati 23:00 ve sonrası ya da 06:00'dan önce.
- yuksek_tutar: tutar 5000 TL ve üzeri.
- sik_islem: aynı kartta, herhangi bir 10 dakikalık pencerede 3 ya da daha fazla işlem; pencereye giren her işlem işaretlenir.
İlk iki kural tek işleme bakar; birer satırlık saf fonksiyonlardır. Üçüncü kural işlemler arası ilişkiye bakar: karta göre grupla, zamana göre sırala, her işlem için "bu işlemle başlayan 10 dakikalık pencerede kaç işlem var?" diye say; 3 ve üstüyse penceredeki tüm işlemleri işaretle.
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta
from decimal import Decimal
@dataclass(frozen=True)
class Transaction:
id: str
kart: str
zaman: datetime
tutar: Decimal
kategori: str
islemler = [
Transaction("T3", "****2222", datetime(2024, 3, 15, 2, 15), Decimal("8500.00"), "kuyumcu"),
Transaction("T4", "****3333", datetime(2024, 3, 15, 23, 50), Decimal("60.00"), "yakit"),
Transaction("T5", "****3333", datetime(2024, 3, 15, 23, 54), Decimal("75.50"), "yakit"),
Transaction("T6", "****3333", datetime(2024, 3, 15, 23, 58), Decimal("90.00"), "market"),
Transaction("T7", "****2222", datetime(2024, 3, 15, 18, 0), Decimal("430.00"), "market"),
]
def gece_mi(t: Transaction) -> bool:
return t.zaman.hour >= 23 or t.zaman.hour < 6
def yuksek_mi(t: Transaction, esik: Decimal = Decimal("5000")) -> bool:
return t.tutar >= esik
def sik_islem_idleri(islemler: list[Transaction], pencere_dk: int = 10, esik: int = 3) -> set[str]:
karta_gore: dict[str, list[Transaction]] = defaultdict(list)
for t in islemler:
karta_gore[t.kart].append(t)
supheli: set[str] = set()
pencere = timedelta(minutes=pencere_dk)
for grup in karta_gore.values():
grup.sort(key=lambda t: t.zaman)
for i, baslangic in enumerate(grup):
icindekiler = [t for t in grup[i:] if t.zaman - baslangic.zaman <= pencere]
if len(icindekiler) >= esik:
supheli.update(t.id for t in icindekiler)
return supheli
def supheli_isaretle(islemler: list[Transaction]) -> dict[str, list[str]]:
sik = sik_islem_idleri(islemler)
sonuc = {}
for t in islemler:
nedenler = []
if gece_mi(t): nedenler.append("gece")
if yuksek_mi(t): nedenler.append("yuksek_tutar")
if t.id in sik: nedenler.append("sik_islem")
if nedenler:
sonuc[t.id] = nedenler
return sonuc
for tid, nedenler in supheli_isaretle(islemler).items():
print(tid, "->", ", ".join(nedenler))
T3 iki kurala birden takıldı (gece ve yüksek tutar); T4-T5-T6 ise hem gece hem sık işlem. T7 temiz. Pencere mantığında grup[i:] dilimi yalnızca ileriye bakar; sıralı liste sayesinde geçmişe bakmaya gerek yoktur.
>= ile > arasındaki farkı iş birimine sor ve testte sabitle. Bu projede üç sınır da dahil (>=) kabul edildi.Parça 5: JSON rapor
Rapor üç bölüm taşır: genel özet (işlem sayısı, toplam tutar), kategori toplamları ve şüpheli işlemler. json.dumps Decimal ve datetime'ı tanımaz; ya rapor sözlüğünü kurarken metne çevirirsin ya da default= parametresine bir dönüştürücü verirsin. Para alanlarını metin olarak yazmak ("8500.00") float'a çevirmekten güvenlidir: alıcı sistem tekrar Decimal'e dönebilir.
import json
from datetime import datetime
from decimal import Decimal
def json_uyumlu(nesne):
if isinstance(nesne, Decimal):
return str(nesne)
if isinstance(nesne, datetime):
return nesne.isoformat()
raise TypeError(f"JSON'a çevrilemez: {type(nesne).__name__}")
rapor = {
"tarih": datetime(2024, 3, 15).date().isoformat(),
"islem_sayisi": 7,
"toplam_tutar": Decimal("10601.40"),
"kategori_toplam": {"kuyumcu": Decimal("8500.00"), "elektronik": Decimal("1200.00")},
"supheli": [
{"id": "T3", "kart": "****2222", "zaman": datetime(2024, 3, 15, 2, 15), "tutar": Decimal("8500.00"), "nedenler": ["gece", "yuksek_tutar"]},
],
}
metin = json.dumps(rapor, default=json_uyumlu, ensure_ascii=False, indent=2)
print(metin)
geri = json.loads(metin)
print("geri okunan toplam:", Decimal(geri["toplam_tutar"]) + Decimal("0.60"))
Parça 6: Testler
Kurallar test edilmeden teslim edilmez. Her kural için en az bir pozitif, bir negatif ve bir sınır testi yazarsın. Test verisi küçük ve elle hesaplanabilir olmalı; büyük CSV'yi teste sokma. Aşağıdaki örnek yalnızca gece kuralını test eder; alıştırmada üç kuralı birden yazacaksın.
import io
import unittest
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
@dataclass(frozen=True)
class Transaction:
id: str
kart: str
zaman: datetime
tutar: Decimal
kategori: str
def gece_mi(t):
return t.zaman.hour >= 23 or t.zaman.hour < 6
def islem(saat, dakika=0, tutar="100"):
return Transaction("X", "****0000", datetime(2024, 3, 15, saat, dakika), Decimal(tutar), "market")
class GeceKuraliTest(unittest.TestCase):
def test_gece_yarisi_sonrasi(self):
self.assertTrue(gece_mi(islem(2, 15)))
def test_sinir_23_dahil(self):
self.assertTrue(gece_mi(islem(23, 0)))
def test_sinir_6_haric(self):
self.assertFalse(gece_mi(islem(6, 0)))
def test_ogle_degil(self):
self.assertFalse(gece_mi(islem(13, 40)))
suite = unittest.defaultTestLoader.loadTestsFromTestCase(GeceKuraliTest)
adlar = [t.id().split(".")[-1] for t in suite]
sonuc = unittest.TextTestRunner(stream=io.StringIO(), verbosity=0).run(suite)
print(len(adlar), "test:", adlar)
print("başarılı:", sonuc.wasSuccessful())
islem(...) yardımcı fonksiyonu test verisini tek satırda kurar; test gövdeleri yalnızca kuralı konuşur. Bu küçük "fabrika" kalıbı, test dosyalarını okunur tutmanın en ucuz yoludur.
Derinleş: Projeyi üretim kalitesine taşımak
Altı parçayla çalışan bir analiz aracın var. Kart operasyon ekibi aracı her gün kullanmaya başlayınca yeni istekler gelir: "Bozuk satır yüzünden tüm rapor çöktü", "Eşiği 5000'den 7500'e çekmek için yazılımcı mı bekleyeceğiz?", "Dünün sonuçlarını nerede saklıyoruz?", "Bunu her sabah komut satırından çalıştırmak istiyoruz". Bu bölüm, orta seviyede öğrendiğin her şeyi (doğrulama, yapılandırma, SQLite, argparse, logging, test, Git) aynı projede bir araya getirir. Alıştırmalar da bu ihtiyaçları birer birer kapatır.
1. Girdiyi doğrula, hatayı topla
Gerçek dosyada bazı satırlar bozuktur: tarih yanlış biçimde, tutar boş, kategori eksik. İki strateji vardır: hızlı başarısız ol (fail fast: ilk hatada dur) ve hataları topla (geçerli satırları işle, bozukları satır numarasıyla raporla). Günlük operasyon raporlarında ikincisi tercih edilir; ama bozuk satır oranı bir eşiği aşarsa (ör. %5) dosyanın tamamı reddedilir, çünkü o noktada sorun tek tük satırda değil, kaynaktadır.
import csv, io
from datetime import datetime
from decimal import Decimal, InvalidOperation
VERI = """id,kart,zaman,tutar,kategori
T1,****1111,2024-03-15 09:12:00,245.90,market
T2,****1111,15.03.2024 13:40,1200.00,elektronik
T3,****2222,2024-03-15 02:15:00,,kuyumcu
T4,****3333,2024-03-15 23:50:00,-60.00,yakit
T5,****3333,2024-03-15 23:54:00,75.50,
"""
gecerli, hatalar = [], []
for no, s in enumerate(csv.DictReader(io.StringIO(VERI)), start=2): # 1. satır başlık
try:
zaman = datetime.strptime(s["zaman"], "%Y-%m-%d %H:%M:%S")
tutar = Decimal(s["tutar"])
if tutar <= 0:
raise ValueError("tutar pozitif olmalı")
if not s["kategori"].strip():
raise ValueError("kategori boş")
except InvalidOperation:
hatalar.append((no, s["id"], "tutar sayı değil"))
continue
except ValueError as e:
hatalar.append((no, s["id"], str(e)[:30]))
continue
gecerli.append((s["id"], zaman.hour, tutar))
print("geçerli:", gecerli)
for h in hatalar:
print("satır", *h)
oran = len(hatalar) / (len(hatalar) + len(gecerli))
print(f"hata oranı %{oran * 100:.0f} ->", "DOSYAYI REDDET" if oran > 0.05 else "devam")
Decimal("") ve Decimal("abc") decimal.InvalidOperation fırlatır; bu sınıf ArithmeticError'dan türer. Yalnızca except ValueError yazarsan boş tutarlı satır tüm programı düşürür.2. Kurallar veri olsun: kayıt defteri ve ayar dosyası
Kural eklemek için kurallari_uygula fonksiyonunu her seferinde değiştirmek yerine kuralları bir kayıt defterine (registry) koy: ad → fonksiyon. Eşikler ise koddan çıkıp bir ayar sözlüğüne (gerçekte JSON/TOML dosyası) taşınır. Ayardan gelen değerler metin ya da float olabileceği için okurken tek noktada doğru tipe çevrilir.
import json
from datetime import datetime
from decimal import Decimal
AYAR_JSON = '{"yuksek_esik": "5000.00", "gece_bas": 23, "gece_bit": 6, "riskli_kategoriler": ["kuyumcu", "kripto"]}'
ham = json.loads(AYAR_JSON)
ayar = {**ham, "yuksek_esik": Decimal(ham["yuksek_esik"]), "riskli_kategoriler": set(ham["riskli_kategoriler"])}
KURALLAR = {}
def kural(ad):
def kaydet(f):
KURALLAR[ad] = f
return f
return kaydet
@kural("gece")
def _gece(t, a):
return t["zaman"].hour >= a["gece_bas"] or t["zaman"].hour < a["gece_bit"]
@kural("yuksek")
def _yuksek(t, a):
return t["tutar"] >= a["yuksek_esik"]
@kural("riskli_kategori")
def _kategori(t, a):
return t["kategori"] in a["riskli_kategoriler"]
islem = {"id": "T3", "zaman": datetime(2024, 3, 15, 2, 15), "tutar": Decimal("8500.00"), "kategori": "kuyumcu"}
print(sorted(KURALLAR))
print([ad for ad, f in KURALLAR.items() if f(islem, ayar)])
3. Sakla: SQLite'ta kuruş olarak
Sonuçları her gün bir SQLite dosyasına yazarsan "geçen hafta kaç şüpheli işlem vardı" sorusu tek sorgu olur. Para için SQLite'ın REAL tipi float'tır; tutarı tam sayı kuruş olarak saklamak hem toplamları kesin yapar hem de Decimal'e geri dönüşü kolaylaştırır. Zamanı ISO 8601 metni olarak yaz.
import sqlite3
from decimal import Decimal
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE islem (id TEXT PRIMARY KEY, zaman TEXT, tutar_kurus INTEGER, kategori TEXT)")
veriler = [("T1", "2024-03-15T09:12:00", Decimal("0.10"), "market"),
("T2", "2024-03-15T13:40:00", Decimal("0.20"), "market")]
con.executemany("INSERT INTO islem VALUES (?, ?, ?, ?)",
[(i, z, int(t * 100), k) for i, z, t, k in veriler])
kurus = con.execute("SELECT SUM(tutar_kurus) FROM islem WHERE kategori = 'market'").fetchone()[0]
print(Decimal(kurus) / 100, "| float olsaydı:", 0.10 + 0.20)
4. Komut satırı aracı ve proje ağacı
Son adım aracı paketlemek. Proje git-proje-yapisi dersindeki src yerleşimine oturur; giriş noktası argparse ile parametre alır, sonucu stdout'a ya da dosyaya yazar, hata durumunda sıfır dışı çıkış kodu döner ki zamanlayıcı (cron, Windows Görev Zamanlayıcı) sorunu fark etsin.
kart-analiz/ ├── pyproject.toml # [project.scripts] kart-analiz = "kart_analiz.cli:main" ├── README.md .gitignore .env.example ├── ayar.json # eşikler: kod değişmeden ayarlanır ├── src/kart_analiz/ │ ├── model.py # Transaction, csv_oku, doğrulama │ ├── kurallar.py # kayıt defteri + kurallar │ ├── rapor.py # JSON / CSV çıktı │ ├── depo.py # SQLite │ └── cli.py # argparse + logging └── tests/ test_model.py test_kurallar.py test_rapor.py # Kendi bilgisayarında: python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 python -m pip install -e . kart-analiz islemler.csv --ayar ayar.json --cikti rapor.json --veritabani gecmis.db -v python -m unittest discover -s tests
| Üretim kontrol listesi | Bu projede karşılığı | İlgili ders |
|---|---|---|
| Bozuk veri programı düşürmüyor | Satır bazında doğrulama, hata raporu, eşik | hatalar, json-csv |
| Eşikler koddan ayrı | ayar.json + tek noktada tip dönüşümü | yapilandirma |
| Para kesin | Decimal; SQLite'ta kuruş, JSON'da metin | standart-kutuphane |
| Ne olduğu izlenebilir | logging: kaç satır okundu, kaç hata, kaç işaret | logging |
| Kurallar güvenilir | Sınır değer testleri, mock'suz saf fonksiyonlar | test-yazma |
| Kurulum tekrarlanabilir | pyproject + kilit dosyası | sanal-ortam-pip |
| Değişiklikler izlenebilir | Git, PR, Conventional Commits | git-proje-yapisi |
Bu projeyi üretime taşısaydın
Gerçek bir kart izleme sistemi aynı iskeleti büyütür. Kurallar bir listede tutulur: her kural (ad, fonksiyon) çifti olur ve supheli_isaretle listeyi dolaşır; yeni kural eklemek if satırı eklemek değil, listeye eleman eklemek haline gelir. Eşikler (5000 TL, 10 dakika, 3 işlem) koda değil yapılandırmaya yazılır; operasyon ekibi kodu değiştirmeden ayarı değiştirir. Sık işlem kuralı milyonlarca işlemde O(n²)'ye yaklaşır; sıralı listede iki işaretçiyle (sliding window) tek geçişe iner, İleri seviyedeki performans dersinin konusu.
Veri CSV'den değil veritabanından gelir: sqlite3 dersindeki iş bölümü burada da geçerli; kart ve tarih aralığı filtresini SQL yapar, kuralları Python uygular. Rapor JSON olarak bir kuyruk ya da API'ye gider, operasyon ekibi panelde görür. Testler CI'da her commit'te koşar; bir kuralın eşiği değişince önce test güncellenir. Bu dersteki altı parça, o sistemin küçültülmüş ama eksiksiz bir modelidir: model, hesap, kural, rapor, test. Bir mülakatta "orta seviye Python projesi anlat" dendiğinde anlatacağın şey tam olarak bu.