Ana içeriğe geç
Kesit Python
İleri · Veri mühendisliği

Veri boru hattı (ETL, data pipeline): çıkar, dönüştür, yükle

Anadolu Market günlük satış ETL'i: pathlib ile dosya düzeni, logging, şema doğrulama, hata kuyruğu, idempotent yükleme (sqlite3) ve zamanlama kavramı.

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

Neden önemli?

Anadolu Market her gece mağaza sistemlerinden gelen satış dosyalarını merkez veritabanına yükler. Bu iş bir kez yazılıp yıllarca her gece çalışır; bu yüzden "çalışıyor" yetmez: dosya yarım gelirse ne olur, aynı dosya iki kez gelirse mükerrer kayıt oluşur mu, bozuk satır tüm yüklemeyi durdurur mu, sabah ekip neyin yüklendiğini nereden görür? Bu derste bir veri boru hattı (data pipeline) kurarken sorulan bu soruların standart cevaplarını öğreneceksin. ETL = Extract (çıkar), Transform (dönüştür), Load (yükle).

Dosya düzeni ve pathlib

Klasik düzen üç klasördür: gelen/ (ham dosyalar bırakılır), islenen/ (başarıyla yüklenenler taşınır), hatali/ (işlenemeyenler ve hata kuyruğu). pathlib.Path ile klasörleri oluştur, dosyaları desenle bul, taşı. Pyodide'de bellek içi dosya sistemi olduğu için aynı kod tarayıcıda da çalışır.

from pathlib import Path
import shutil

KOK = Path('etl_demo')
GELEN, ISLENEN, HATALI = KOK / 'gelen', KOK / 'islenen', KOK / 'hatali'
for k in (GELEN, ISLENEN, HATALI):
    k.mkdir(parents=True, exist_ok=True)

(GELEN / 'satis_2024-05-01.csv').write_text(
    'siparis_no,sehir,tutar,adet\n1001,İzmir,149.90,2\n1002,Bursa,80.00,1\n', encoding='utf-8')
(GELEN / 'satis_2024-05-02.csv').write_text(
    'siparis_no,sehir,tutar,adet\n1003,Ankara,abc,1\n1004,İzmir,20.5,3\n', encoding='utf-8')
(GELEN / 'notlar.txt').write_text('bu dosya ETL konusu değil')

for p in sorted(GELEN.glob('satis_*.csv')):
    print(p.name, p.stem, p.suffix, p.stat().st_size, 'bayt')
shutil.move(str(GELEN / 'notlar.txt'), str(HATALI / 'notlar.txt'))
print('hatali:', [p.name for p in HATALI.iterdir()])
Dosya adında tarihsatis_2024-05-01.csv gibi ISO tarihli adlar hem sıralamayı hem "bu dosya daha önce yüklendi mi?" sorusunu kolaylaştırır. İş tarihi (business date) ile dosyanın geliş zamanını karıştırma; ikisini de kaydet.

Extract ve şema doğrulama

Çıkarma adımı ham satırları okur; dönüştürme adımı her satırı şemaya göre doğrular ve tipler. Bozuk satır tüm işi durdurmamalı; hata kuyruğuna (dead-letter queue) yazılıp iş devam etmeli. Her hata kaydı: kaynak dosya, satır numarası, ham veri, hata nedeni.

import csv, io
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation

@dataclass(frozen=True)
class Satis:
    siparis_no: int
    sehir: str
    tutar: Decimal
    adet: int

SEHIRLER = {'İzmir', 'Bursa', 'Ankara', 'İstanbul'}

def dogrula(ham: dict) -> Satis:
    try:
        no = int(ham['siparis_no'])
        tutar = Decimal(ham['tutar'])
        adet = int(ham['adet'])
    except (KeyError, ValueError, InvalidOperation) as e:
        raise ValueError(f'tip hatası: {e}') from e
    if tutar < 0: raise ValueError('tutar negatif')
    if adet <= 0: raise ValueError('adet pozitif olmalı')
    sehir = ham['sehir'].strip()
    if sehir not in SEHIRLER: raise ValueError(f'bilinmeyen şehir: {sehir}')
    return Satis(no, sehir, tutar, adet)

def extract_transform(metin: str, kaynak: str):
    gecerli, hatalar = [], []
    for i, ham in enumerate(csv.DictReader(io.StringIO(metin)), start=2):   # 1 = başlık
        try:
            gecerli.append(dogrula(ham))
        except ValueError as e:
            hatalar.append({'kaynak': kaynak, 'satir': i, 'ham': ham, 'neden': str(e)})
    return gecerli, hatalar

metin = 'siparis_no,sehir,tutar,adet\n1003,Ankara,abc,1\n1004,İzmir,20.5,3\n1005,Mars,5,1\n'
g, h = extract_transform(metin, 'satis_2024-05-02.csv')
print('geçerli:', g)
for x in h:
    print('HATA', x['satir'], x['neden'])
Para için DecimalSatış tutarlarını float ile toplarsan kuruş farkları birikir. Dönüştürme adımında Decimal'e çevir, veritabanına kuruş cinsinden tam sayı ya da metin olarak yaz.

Load: idempotent yükleme

İdempotent yükleme, aynı dosyayı iki kez çalıştırınca sonucun değişmemesi demektir. İki araç: tabloda doğal anahtar üzerinde UNIQUE kısıtı ve INSERT ... ON CONFLICT DO UPDATE (upsert); ayrıca hangi dosyanın yüklendiğini tutan bir yukleme_kaydi tablosu. Yükleme bir işlem (transaction) içinde yapılır: ya hepsi ya hiçbiri.

import sqlite3
from decimal import Decimal

def semayi_kur(con):
    con.executescript('''
        CREATE TABLE IF NOT EXISTS satis (
            siparis_no INTEGER PRIMARY KEY, sehir TEXT NOT NULL,
            tutar_kurus INTEGER NOT NULL, adet INTEGER NOT NULL, kaynak TEXT NOT NULL);
        CREATE TABLE IF NOT EXISTS yukleme_kaydi (
            kaynak TEXT PRIMARY KEY, satir INTEGER, hata INTEGER, yuklendi_utc TEXT);
    ''')

def load(con, satislar, kaynak, hata_sayisi):
    with con:                                   # transaction: hata olursa geri alınır
        con.executemany('''
            INSERT INTO satis (siparis_no, sehir, tutar_kurus, adet, kaynak)
            VALUES (?, ?, ?, ?, ?)
            ON CONFLICT(siparis_no) DO UPDATE SET
                sehir=excluded.sehir, tutar_kurus=excluded.tutar_kurus,
                adet=excluded.adet, kaynak=excluded.kaynak
        ''', [(s.siparis_no, s.sehir, int(s.tutar * 100), s.adet, kaynak) for s in satislar])
        con.execute('''INSERT INTO yukleme_kaydi VALUES (?, ?, ?, datetime('now'))
                       ON CONFLICT(kaynak) DO UPDATE SET satir=excluded.satir, hata=excluded.hata,
                       yuklendi_utc=excluded.yuklendi_utc''', (kaynak, len(satislar), hata_sayisi))

con = sqlite3.connect(':memory:')
semayi_kur(con)
veri = [Satis(1001, 'İzmir', Decimal('149.90'), 2), Satis(1002, 'Bursa', Decimal('80.00'), 1)]
load(con, veri, 'satis_2024-05-01.csv', 0)
load(con, veri, 'satis_2024-05-01.csv', 0)     # ikinci çalıştırma: mükerrer yok
print(con.execute('SELECT COUNT(*), SUM(tutar_kurus) FROM satis').fetchone())
print(con.execute('SELECT kaynak, satir FROM yukleme_kaydi').fetchall())
Yarım yüklemewith con: bloğu istisna durumunda ROLLBACK yapar. Dosyayı islenen/ klasörüne taşıma işlemini yalnızca commit başarılı olduktan sonra yap; yoksa veritabanında olmayan bir dosya "işlendi" görünür.

logging ile izlenebilirlik

print yerine logging: seviye (INFO/WARNING/ERROR), zaman damgası, modül adı, ve üretimde dosyaya ya da merkezi log sistemine yönlendirme. Her çalıştırma için bir çalıştırma kimliği (run id) üret; sabah ekip "dün gece 03:00 çalıştırması" diye arar.

import logging, io, uuid

akis = io.StringIO()
logging.basicConfig(stream=akis, level=logging.INFO,
                    format='%(asctime)s %(levelname)s %(name)s: %(message)s', force=True)
log = logging.getLogger('etl.satis')

run_id = uuid.UUID(int=12345)            # gerçekte uuid.uuid4()
log.info('başladı run_id=%s', run_id)
log.info('dosya=%s geçerli=%d hata=%d', 'satis_2024-05-02.csv', 1, 2)
log.warning('hata kuyruğuna yazıldı: %d satır', 2)
log.error('dosya okunamadı: %s', 'satis_2024-05-03.csv')
print(akis.getvalue())
AdımGirdiÇıktıHata durumunda
Extractgelen/*.csvham satır listesiDosya hatali/ klasörüne, ERROR log
Transformham satırlarSatis nesneleri + hata kuyruğuSatır kuyruğa, WARNING log, iş devam
LoadSatis nesneleriDB satırları + yükleme kaydıROLLBACK, dosya gelen/ içinde kalır
Arşivişlenen dosyaislenen/ klasörüYalnızca commit sonrası

Uçtan uca: Anadolu Market günlük satış ETL'i

import sqlite3, shutil, json, logging, io
from pathlib import Path

def calistir(kok: Path, con, log):
    gelen, islenen, hatali = kok / 'gelen', kok / 'islenen', kok / 'hatali'
    for k in (gelen, islenen, hatali): k.mkdir(parents=True, exist_ok=True)
    ozet = []
    for dosya in sorted(gelen.glob('satis_*.csv')):
        gecerli, hatalar = extract_transform(dosya.read_text(encoding='utf-8'), dosya.name)
        if hatalar:
            (hatali / (dosya.stem + '.hatalar.jsonl')).write_text(
                '\n'.join(json.dumps(h, ensure_ascii=False) for h in hatalar), encoding='utf-8')
        load(con, gecerli, dosya.name, len(hatalar))
        shutil.move(str(dosya), str(islenen / dosya.name))
        log.info('%s: %d yüklendi, %d hatalı', dosya.name, len(gecerli), len(hatalar))
        ozet.append((dosya.name, len(gecerli), len(hatalar)))
    return ozet

akis = io.StringIO()
logging.basicConfig(stream=akis, level=logging.INFO, format='%(levelname)s %(message)s', force=True)
con = sqlite3.connect(':memory:'); semayi_kur(con)
print(calistir(Path('etl_demo'), con, logging.getLogger('etl')))
print('kalan gelen:', [p.name for p in Path('etl_demo/gelen').iterdir()])
print(con.execute('SELECT sehir, SUM(tutar_kurus)/100.0 FROM satis GROUP BY sehir').fetchall())
print(akis.getvalue().strip())

Zamanlama: cron ve Airflow

Betiği her gece çalıştırmak için en basit araç cron'dur: 0 3 * * * /opt/etl/.venv/bin/python -m etl.satis satırı her gün 03:00'te çalıştırır. Boru hattı büyüyünce (bağımlılıklar, yeniden deneme, izleme, geriye dönük çalıştırma) Airflow, Prefect ya da Dagster gibi orkestratörler kullanılır: her adım bir görev (task), görevler yönlü çevrimsiz çizge (DAG) oluşturur, başarısız görev tek başına yeniden denenir. İdempotent yazılmış bir ETL, bu araçlarla sorunsuz çalışır; idempotent olmayan ise her yeniden denemede mükerrer kayıt üretir.

Derinleş

Tam yükleme mi, artımlı yükleme mi?

Derste her gün gelen dosyayı yükledin. Kaynak bir veritabanı ya da API olduğunda iki yol var: her gece tüm tabloyu çekmek (tam yükleme, full load) ya da yalnız son çalıştırmadan beri değişenleri çekmek (artımlı yükleme, incremental load). Küçük tablolarda tam yükleme basit ve güvenlidir. Milyonlarca satırda artımlı yükleme şarttır; bunun için bir su seviyesi işareti (watermark, high-water mark) tutulur: "en son hangi guncellenme zamanına kadar aldım?"

YöntemNasıl?ArtısıDikkat
Tam yüklemeHedefi boşalt, hepsini yükleBasit, silinen kayıtlar da yansırYavaş; yüklerken tablo boş kalmasın (geçici tablo + yer değiştir)
Artımlı (watermark)WHERE guncellenme > :sonHızlı, kaynağa az yükKaynakta silinen satırları görmez; aynı zaman damgasındaki satırlar kaçabilir
Değişiklik yakalama (CDC)Veritabanı günlüğünden ekle/güncelle/sil olaylarını okuSilmeler dahil her değişiklikEk altyapı (Debezium vb.)
Yavaş değişen boyut (SCD 2)Değişen kaydı kapat, yeni sürüm açGeçmiş korunur ("Mart'ta müşteri hangi şehirdeydi?")Sorgular geçerlilik tarihini hesaba katmalı

Watermark'ı yalnızca yükleme başarıyla bittikten sonra ilerlet; aksi hâlde yarım kalan çalıştırmanın kayıtları sonsuza kadar kaçar. Aynı zaman damgasına sahip satırlar için (guncellenme, id) çiftini watermark olarak tutmak ya da >= ile çekip upsert ile tekrarları zararsız yapmak iki yaygın çözümdür.

import sqlite3, json

kaynak = sqlite3.connect(":memory:")
kaynak.execute("CREATE TABLE musteri(id INTEGER PRIMARY KEY, sehir TEXT, guncellenme TEXT)")
kaynak.executemany("INSERT INTO musteri VALUES (?,?,?)", [
    (1, "İzmir", "2024-05-01T10:00"), (2, "Bursa", "2024-05-01T12:00"),
    (3, "Ankara", "2024-05-02T09:00")])

durum = {"watermark": "2024-05-01T11:00"}          # gerçekte dosya/tabloda saklanır

def artimli_cek(con, son):
    return con.execute("SELECT id, sehir, guncellenme FROM musteri "
                       "WHERE guncellenme > ? ORDER BY guncellenme", (son,)).fetchall()

yeni = artimli_cek(kaynak, durum["watermark"])
print("çekilen:", [r[0] for r in yeni])
# ... yükleme başarılı olduktan SONRA:
if yeni:
    durum["watermark"] = yeni[-1][2]
print(json.dumps(durum))

Büyük dosyalar: akış ve parti

10 GB'lık bir CSV'yi list(csv.DictReader(f)) ile belleğe almak sunucuyu düşürür. Üreteçlerle (iteratorler-uretecler dersi) her adım bir sonrakine satır satır akar; bellekte aynı anda yalnız bir parti durur. Veritabanına da satır satır değil parti parti (batch) yazılır: executemany ile 500-5000 satırlık partiler, her parti bir işlem (transaction) içinde. Python 3.12'deki itertools.batched bu işi tek satıra indirir.

import csv, io, sqlite3
from itertools import batched

ham = "no,tutar\n" + "\n".join(f"{i},{i * 1.5}" for i in range(1, 2501))

def oku(metin):                       # gerçekte: open(yol, newline="", encoding="utf-8")
    yield from csv.DictReader(io.StringIO(metin))

def donustur(satirlar):
    for s in satirlar:
        yield int(s["no"]), round(float(s["tutar"]) * 100)     # kuruş

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE satis(no INTEGER PRIMARY KEY, tutar_kurus INTEGER)")
parti_sayisi = 0
for parti in batched(donustur(oku(ham)), 1000):
    with con:                          # her parti ayrı işlem: ya hep ya hiç
        con.executemany("INSERT OR REPLACE INTO satis VALUES (?, ?)", parti)
    parti_sayisi += 1
print(parti_sayisi, "parti,", con.execute("SELECT COUNT(*), SUM(tutar_kurus) FROM satis").fetchone())
pandas ile parça parçapandas kullanıyorsan aynı fikir pd.read_csv(yol, chunksize=100_000) ile gelir: her döngüde bir DataFrame parçası alırsın. Sütunlu ve sıkıştırılmış biçim için ara katmanda CSV yerine Parquet kullan (pip install pyarrow, df.to_parquet("satis.parquet")); hem 5-10 kat küçük hem tipleri korur. Kurulumda pyarrow tekerleği bulunamazsa Python sürümünün desteklendiğini kontrol et ve pip'i güncelle: python -m pip install -U pip.

Veri kalitesi kapıları

Boru hattı hata vermeden bitti diye veri doğru değildir. Kaynak sistem bir gece sütun sırasını değiştirir, tutarlar kuruş yerine lira gelir ya da dosya yarım kesilir. Yükleme sonrası (ya da yayına almadan önce) küçük kontroller çalıştır ve eşik aşılırsa işi başarısız say:

  • Satır sayısı mutabakatı: kaynaktaki satır = yüklenen + reddedilen. Fark varsa satır kaybolmuştur.
  • Boşluk oranı: zorunlu sütunda None oranı eşiğin (ör. %1) üstündeyse dur.
  • Tekillik: anahtar sütunda tekrar olmamalı.
  • Aralık ve küme: tutar 0 ile makul bir üst sınır arasında, şehir bilinen listede.
  • Tazelik: en yeni kaydın tarihi beklenen iş gününden eski olmamalı.
  • Hacim: bugünkü satır sayısı son 7 günün ortalamasının yarısından azsa kaynakta sorun olabilir.

Bu kontroller büyüyünce Great Expectations, Soda ya da dbt testleri gibi araçlara taşınır (orkestrasyon dersindeki kalite kapıları); fikir aynıdır.

Atomik yazma ve dosya parmak izi

Çıktı dosyasını doğrudan hedef ada yazarken iş yarıda kesilirse, sonraki adım yarım bir dosya okur. Doğru yol: önce aynı klasörde geçici bir dosyaya yaz, tamamlanınca os.replace(gecici, hedef) ile adını değiştir. Aynı dosya sistemi içinde ad değiştirme atomiktir: okuyan ya eski dosyayı ya yeni dosyayı görür, yarısını asla. Gelen dosyalar için de parmak izi (SHA-256 özeti) tut: aynı içerik farklı adla ikinci kez gelirse tanırsın.

import hashlib, os, tempfile
from pathlib import Path

def atomik_yaz(yol: Path, metin: str) -> None:
    fd, gecici = tempfile.mkstemp(dir=yol.parent, suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(metin)
            f.flush()
            os.fsync(f.fileno())       # diske gerçekten yazıldığından emin ol
        os.replace(gecici, yol)        # atomik ad değiştirme
    except BaseException:
        os.unlink(gecici)
        raise

def parmak_izi(yol: Path) -> str:
    h = hashlib.sha256()
    with open(yol, "rb") as f:
        for parca in iter(lambda: f.read(65536), b""):
            h.update(parca)
    return h.hexdigest()

klasor = Path("etl_cikti"); klasor.mkdir(exist_ok=True)
atomik_yaz(klasor / "rapor.csv", "sehir,ciro\nİzmir,1250\n")
atomik_yaz(klasor / "rapor_kopya.csv", "sehir,ciro\nİzmir,1250\n")
print(parmak_izi(klasor / "rapor.csv") == parmak_izi(klasor / "rapor_kopya.csv"))
print(sorted(p.name for p in klasor.iterdir()))
Windows ve ağ sürücülerios.replace aynı dosya sistemi içinde atomiktir; geçici dosyayı hedefle aynı klasörde oluşturmanın nedeni budur (/tmp başka bir diskte olabilir). Windows'ta hedef dosya başka bir programda (ör. Excel'de) açıksa PermissionError alırsın; raporu açık tutan kullanıcılar için yeniden deneme ya da tarihli dosya adı kullan.
Gerçek işteVeri ekiplerinde en pahalı hatalar sessiz olanlardır: iş "başarılı" biter ama yarım dosya, kaçan artımlı kayıtlar ya da iki kez yüklenen bir gün raporlara girer ve haftalar sonra fark edilir. Her çalıştırmanın sonunda bir özet satırı logla (kaynak satır, yüklenen, reddedilen, watermark, süre) ve kalite kapılarından biri kırmızıysa işi başarısız say. "Sessiz başarı" yerine "gürültülü başarısızlık" tercih edilir.
Derinleş

Derste kalite kapılarını bir kontrol listesi olarak gördün. Bu bölüm onları uygulamaya döküyor: kalitenin boyutlarını ölçmek, kuralları koddan ayırıp bir şemaya yazmak, kötü satırları karantinaya almak, üretici ile tüketici arasındaki beklentiyi bir veri sözleşmesine bağlamak ve her çalıştırmanın sonunda okunabilir bir rapor üretmek. Örnekler gerçek kirli tablolarla: Anadolu Market'in raw_signups kayıt dökümü ve Power Query'den tanıdığın kirli_veri bayi raporu.

Kalitenin boyutları

BoyutSoruraw_signups'ta örnek
Tamlık (completeness)Zorunlu alanlar dolu mu?10 satırda şehir boş
Tekillik (uniqueness)Anahtar tekrar ediyor mu?52 satır, 40 benzersiz id
Geçerlilik (validity)Değer biçime ve kurala uyuyor mu?Tarih kimi satırda 29.07.2024, kiminde 2024-02-10
Tutarlılık (consistency)Aynı şey hep aynı mı yazılmış?ISTANBUL, istanbul, İstanbul
Doğruluk (accuracy)Değer gerçeği yansıtıyor mu?Kaynakla mutabakat gerekir; tek başına ölçülemez
Tazelik (timeliness)Veri yeterince güncel mi?En yeni kayıt beklenen günden eski mi?

Her boyutu bir orana çevirmek (ör. tamlık = zorunlu alanları dolu satır / tüm satırlar) kaliteyi zaman içinde izlenebilir kılar: "bu hafta tamlık %98'den %81'e düştü" demek, "veride sorun var" demekten çok daha işe yarar.

import veri, re
from collections import Counter
r = veri.kayitlar("anadolu_market", "raw_signups")
n = len(r)
tamlik = sum(x["city"] is not None for x in r) / n
tekillik = len({x["id"] for x in r}) / n
gecerlilik = sum(bool(re.fullmatch(r"\d{4}-\d{2}-\d{2}", x["signup_date"])) for x in r) / n
yazimlar = Counter(x["city"] for x in r if x["city"])
print(f"satır={n} tamlık={tamlik:.1%} tekillik={tekillik:.1%} ISO tarih={gecerlilik:.1%}")
print("şehir yazımları:", len(yazimlar), sorted(yazimlar)[:4])

Kurallar koddan ayrı: şema

Kuralları if yığınları olarak yazmak yerine bir şemada (schema) toplarsan hem okunur hem yeniden kullanılır. Python dünyasında bunun en yaygın aracı pandera'dır (pandas/polars DataFrame şeması); satır bazlı API girdisinde ise pydantic. Mantık aynı: sütun → tür + kurallar.

import pandera.pandas as pa   # pip install pandera
sema = pa.DataFrameSchema({
    "id":          pa.Column(int, unique=True),
    "email":       pa.Column(str, pa.Check.str_matches(r"^[^@\s]+@[^@\s]+\.[a-z]+$")),
    "city":        pa.Column(str, pa.Check.isin(["İstanbul", "Ankara", "İzmir"]), nullable=False),
    "signup_date": pa.Column("datetime64[ns]"),
}, strict=True)                # şemada olmayan sütun gelirse hata
try:
    sema.validate(df, lazy=True)          # lazy: ilk hatada durma, hepsini topla
except pa.errors.SchemaErrors as e:
    print(e.failure_cases)                # hangi sütun, hangi kural, hangi değer
Kendi bilgisayarında denepandera tarayıcıda yüklü değil. Aşağıdaki alıştırmalarda aynı fikri saf Python ile, sözlükten oluşan bir şemayla kuracaksın; böylece kütüphanenin içinde ne olduğunu da görmüş olacaksın.

Karantina tablosu: ne at, ne durdur

Kötü satırla ne yapmalı? Sessizce atmak veri kaybettirir ve kimse fark etmez; tüm partiyi durdurmak tek bir bozuk satır için bütün raporu bekletir. Orta yol karantinadır (quarantine): geçen satırlar hedefe, kalanlar ham hâliyle ve neden bilgisiyle ayrı bir tabloya yazılır. Veri sahibi karantinayı inceler, kaynağı düzeltir, satırlar yeniden işlenir. Karantina oranı eşiği aşarsa (ör. %5) parti yine durdurulur; bu, derste gördüğün hata kuyruğunun veritabanındaki karşılığıdır.

import sqlite3, json
c = sqlite3.connect(":memory:")
c.execute("CREATE TABLE karantina (parti TEXT, kayit TEXT, nedenler TEXT)")
bayi = veri.kayitlar("kirli_veri", "bayi")[2:]          # ilk iki satır başlık
for satir in bayi:
    nedenler = []
    if not satir["Column1"]:
        nedenler.append("bayi boş")
    if satir["Column4"] in (None, "-"):
        nedenler.append("tutar yok")
    if satir["Column3"] and not re.fullmatch(r"\d{2}\.\d{2}\.\d{4}", satir["Column3"]):
        nedenler.append("tarih geçersiz")
    if nedenler:
        c.execute("INSERT INTO karantina VALUES (?, ?, ?)",
                  ("2024-01", json.dumps(satir, ensure_ascii=False), ", ".join(nedenler)))
for parti, nedenler in c.execute("SELECT parti, nedenler FROM karantina"):
    print(parti, "|", nedenler)

Veri sözleşmesi (data contract)

Kaynak ekip bir gün sütun adını değiştirir ve senin boru hattın kırılır. Veri sözleşmesi, üretici ile tüketici arasında bu beklentileri yazıya döker: şema, kalite kuralları, tazelik taahhüdü (SLA), sahip ve iletişim. Çoğunlukla YAML'dır, git'te durur, değişiklikleri kod incelemesinden (code review) geçer ve CI'da otomatik denetlenir.

# contracts/raw_signups.yaml
dataset: anadolu_market.raw_signups
owner: crm-ekibi@anadolu.example
sla: {tazelik_saat: 24}
columns:
  - {name: id, type: int, required: true, unique: true}
  - {name: email, type: str, required: true, pattern: "^[^@ ]+@[^@ ]+$"}
  - {name: city, type: str, required: true, allowed: [İstanbul, Ankara, İzmir]}
  - {name: signup_date, type: str, required: true, pattern: "^\\d{4}-\\d{2}-\\d{2}$"}
quality:
  max_karantina_orani: 0.05

Sözleşmeyi koda çevirmek, şema doğrulayıcını YAML'dan beslemek demektir: yaml.safe_load ile oku, her sütun tanımını bir kurala dönüştür, sonuçları bir rapora topla. Raporun sonunda tek bir karar olmalı: GEÇTİ ya da KALDI. Bu raporu JSON olarak saklamak, kalitenin zaman içindeki eğilimini panoda göstermeyi sağlar.

Normalleştir, sonra doğrula" Ankara" ve "izmir " gibi değerler gerçekte geçerlidir; yalnız yazım bozuk. Doğrulamadan önce kırpma, büyük/küçük harf ve tarih biçimi gibi güvenli dönüşümleri yap; ama "anlamı tahmin eden" düzeltmeleri (boş şehri "İstanbul" saymak) asla otomatik yapma.
Gerçek işteVeri ekiplerinde Great Expectations, Soda, dbt testleri ve pandera bu işi yapar; veri sözleşmeleri için açık standartlar (Open Data Contract Standard) da var. Hangisini kullanırsan kullan, mülakatta ve işte sorulan şey aynıdır: "Kötü veriyi nasıl yakalıyorsun, nereye koyuyorsun, kime haber veriyorsun?"
Uzmanlaş

Üretim ETL'inde olgunluk işaretleri

İyi bir boru hattı; yapılandırmayı koddan ayırır (ortam değişkeni, TOML), her çalıştırma için metrik üretir (satır sayısı, süre, hata oranı) ve bu metrikleri bir panoya ya da uyarı sistemine gönderir. Hata oranı bir eşiği aşarsa (örneğin satırların yüzde biri) yükleme durdurulur; sessizce yüzde kırk hatalı dosya yüklemek, hiç yüklememekten kötüdür.

Veri kalitesi kontrolleri (data quality checks) yüklemeden sonra çalışır: bugünün toplamı dünün yüzde ellisinden azsa uyar, şehir listesinde bilinmeyen değer varsa uyar. Great Expectations ve pandera bu kontrolleri bildirimsel yazmanı sağlar. Büyük hacimde satır satır Python yerine pandas (ya da Polars) ile vektörel dönüştürme ve to_sql/COPY ile toplu yükleme yapılır; ancak hata kuyruğu, idempotency ve log disiplini aynı kalır.

Geriye dönük çalıştırma (backfill) için betik tarih parametresi almalı: python -m etl.satis --tarih 2024-05-01. Bugünün tarihini kodun içinde date.today() ile sabitlemek, bir hafta sonra "geçen pazartesiyi yeniden yükle" ihtiyacını imkânsız kılar.

Alıştırmalara geç (30)