İçeriğe geç
academia.sh

Ders 10 / 16

Test Edilebilir Dönüşümler

Dönüşümün kendi çıktısına konan beklentilerin yakaladığının ve yakalamadığının sayılması: altı beklenti sağlam hatta hiç ateşlemiyor ve altı bozulma sınıfından dördünü yakalıyor, yinelenme ile tür kayması dönüşümün kendisi tarafından yutulduğu için çıktıda hiç görünmüyor. Birim değişimi çıktıya konan beklentiyle üçüncü aşamada yakalanıyor ve bunun için hiçbir taban çizgisi gerekmiyor, oysa sunum birebir aynı kalıyor. Dönem etiketini bir dönem geriye kaydıran kural hatası 2200 satırı, 55 hücreyi ve beş sunum değerini birebir aynı bıraktığı için altı beklentinin hiçbirine takılmıyor; onu yalnız sabit girdi üzerindeki birim testi yakalıyor.

İçindekiler

Üç derste dönüşümün yeri yazıldı: kaynakta mı hedefte mi yapılacağı, hangi sırayla koşacağı ve hangi katmanda duracağı. Üç sorunun da yanıtı bir düzenekti ve üçü de aynı şeyi varsaydı — kuralın kendisi doğru, veri de düzgün geliyor. Hiçbir aşamada dönüşümün doğru sonucu ürettiği sınanmadı.

Bu ders o soruyu sorar ve iki ayrı yanıt aracını yan yana koyar: elle yazılmış sabit bir girdi üzerinde koşan birim testi ile koşumun gerçek çıktısına konan beklenti. İkisinin yakaladıkları farklıdır, ve daha önemlisi, ikisinin birden göremediği bir kalıntı vardır.

  • DN41. Kaynak, beş aşamalı hat ve bozulma sınıfları kurgudur; gerçek bir depo, dönüştürme çerçevesi ya da veri testi kitaplığı çağrılmaz. Tohum 20260218, çıktı koşumdan bağımsızdır.
  • DN42. Bu derste dönüşüm, ilk üç aşamanın bileşkesidir: ayikla, tekille ve dönem farkından tüketim. Girdisi ham satır, çıktısı tüketim satırıdır.
  • DN43. Beklenti (expectation) çıktının taşıması gereken bir değişmezi doğru ya da yanlış olarak döndüren yordamdır. Burada True bozuldu demektir.
  • DN44. Birim testi yordamı Yazılım Kalitesi ve Test müfredatındaki Birim Testi ve Test Odaklı Geliştirme kursunda, kalite boyutlarının denetime çevrilmesi Veri Mühendisliğine Giriş kursunda ölçüldü ve tekrarlanmaz. Buradaki fark tek bir noktadadır: o denetimler kaynağa bakıyordu, bunlar dönüşümün çıktısına bakar.
  • DN45. Taban çizgisi beklentisiz dönüşümdür: bozulan beklenti 0, çünkü yazılmış beklenti yoktur.
  • DN46. Bozulma sınıfları saldırı değil üretici tarafı kazalarıdır ve her biri tek bir dönemi (2026-07) bozar.
  • DN47. Tüketim tavanı 200 fiziksel bir seçimdir; kaynağın üretecinden bilinen 49 değildir. Beklentiyi yazan, üreteci değil sayacı bilir.
  • DN48. Çözünürlük: tüketim satırı oranının adımı 1/2200 = 0,0005, özet 55 hücre, sunum 5 değer. Sunum üzerinde oran basılmaz, değerin kendisi yazılır.

Sınanabilir Dönüşümün Koşulu

Bir dönüşüm ancak saf fonksiyon olduğunda sınanabilir: aynı girdiye her koşumda aynı çıktıyı verir, gizli durum taşımaz, saate bakmaz, dışarıdan bir şey okumaz. Bu koşul kursun kuralının doğrudan karşılığıdır — yeniden koşturulduğunda aynı sonucu vermeyen bir dönüşümün sınanacak bir sonucu yoktur, çünkü her koşum başka bir cevabı savunulabilir kılar.

Koşul sağlandığında iki ayrı test türü mümkün olur ve ikisi farklı sorulara bakar:

Tür Girdi Kâhin (oracle): beklenen sonuç nereden Yakaladığı
birim testi elle yazılmış sabit girdi elle hesaplanmış çıktı kuralın hatası
veri testi (beklenti) koşumun gerçek çıktısı çıktının taşıması gereken değişmez verinin bozulması

Ayrım kâhin sütununda durur. Birim testinde doğru cevap testi yazandan gelir ve girdi küçük tutulduğu için elle hesaplanabilir. Beklentide böyle bir cevap yoktur; iki bin küçük satırın doğru toplamını kimse elle bilemez. Beklenti yalnız çıktının kendisi hakkında her koşumda doğru olması gereken şeyi yazabilir: anahtar tekildir, tüketim eksi olamaz, hücre sayısı tamdır.

Aşağıdaki blok kaynağı, beş aşamayı ve altı bozulma sınıfını kurar. Ders boyunca kullanılan tanım budur.

# KURGUDUR. Olcum aginin sayac okumalari, bes asamali hat ve bozulma siniflari
# ders icinde tanimlanir; gercek bir depo, cerceve ya da orkestrator cagrilmaz.
TOHUM, M32 = 20260218, 0xFFFFFFFF


def uretec(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def sonraki():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return sonraki


BOLGE = ["dogu", "bati", "kuzey", "guney", "merkez"]
TIP = ["mesken", "isyeri"]
DONEM = [f"2026-{a:02d}" for a in range(1, 13)]
ABONE = {}
for i in range(200):
    r = uretec(TOHUM + 37 * i)
    ABONE[f"A{i + 1:03d}"] = {"bolge": BOLGE[int(r() * 5)], "tip": TIP[int(r() * 2)],
                              "baslangic": 10000 + int(r() * 60000)}


def kaynak_uret():
    kayit = []
    for i, (ab, a) in enumerate(ABONE.items()):
        e = a["baslangic"]
        for d in DONEM:
            r = uretec(TOHUM + 101 * i + 7 * DONEM.index(d))
            e += 4 + int(r() * 46)
            kayit.append({"abone": ab, "donem": d, "bolge": a["bolge"], "tip": a["tip"],
                          "endeks": e, "birim": "m3"})
    return kayit


BOZULMA = ["gec_gelen", "yinelenen", "sema_degisti", "birim_degisti", "eksik_bolum", "kayan_tip"]


def boz(kayit, sinif, donem="2026-07"):
    """Tek bir donemi bozar , kalan donemler saglam kalir."""
    yeni, sayac = [], 0
    for k in kayit:
        if k["donem"] != donem:
            yeni.append(k)
            continue
        if sinif == "gec_gelen":
            if k["abone"] <= "A040":                # bu donemde 40 abone hic gelmedi
                sayac += 1
                continue
            yeni.append(k)
        elif sinif == "yinelenen":
            yeni.append(k)
            if k["abone"] <= "A060":                # 60 kayit iki kez geldi
                yeni.append(dict(k))
                sayac += 1
        elif sinif == "sema_degisti":
            y = dict(k)
            y["endeks_degeri"] = y.pop("endeks")    # alan adi degisti
            sayac += 1
            yeni.append(y)
        elif sinif == "birim_degisti":
            y = dict(k)
            y["endeks"] = k["endeks"] * 1000        # m3 yerine litre
            y["birim"] = "m3"                       # ETIKET DEGISMEDI , sessiz bozulma
            sayac += 1
            yeni.append(y)
        elif sinif == "eksik_bolum":
            if k["bolge"] == "kuzey":               # bir bolgenin bolumu hic yazilmadi
                sayac += 1
                continue
            yeni.append(k)
        elif sinif == "kayan_tip":
            y = dict(k)
            y["endeks"] = str(k["endeks"])          # sayi metne dondu
            sayac += 1
            yeni.append(y)
    return yeni, sayac


def a1_ayikla(kayit):
    """Zorunlu alanlari tasimayan satir dusurulur."""
    ZOR = ("abone", "donem", "bolge", "endeks", "birim")
    tut = [k for k in kayit if all(a in k for a in ZOR)]
    return tut, len(kayit) - len(tut)


def a2_tekille(kayit):
    gor, tut = set(), []
    for k in kayit:
        ad = (k["abone"], k["donem"])
        if ad in gor:
            continue
        gor.add(ad)
        tut.append(k)
    return tut, len(kayit) - len(tut)


def a3_tuketim(kayit):
    """Donem farkindan tuketim. Onceki donem yoksa satir hesaplanamaz."""
    tablo = {(k["abone"], k["donem"]): k for k in kayit}
    cikti, dusen = [], 0
    for k in kayit:
        i = DONEM.index(k["donem"])
        if i == 0:
            continue
        onceki = tablo.get((k["abone"], DONEM[i - 1]))
        if onceki is None:
            dusen += 1
            continue
        try:
            f = int(k["endeks"]) - int(onceki["endeks"])
        except (TypeError, ValueError):
            dusen += 1
            continue
        cikti.append(dict(k, m3=f))
    return cikti, dusen


def a4_ozet(kayit):
    top = {}
    for k in kayit:
        ad = (k["bolge"], k["donem"])
        top[ad] = top.get(ad, 0) + k["m3"]
    return top, 0


def a5_sun(ozet):
    """Sunum: bolge basina donem ortalamasi."""
    bolge = {}
    for (b, d), v in ozet.items():
        bolge.setdefault(b, []).append(v)
    return {b: round(sum(v) / len(v), 2) for b, v in bolge.items()}, 0

Sabit Girdi Üzerinde Birim Testi

İkinci blok sekiz satırlık bir girdi yazar ve doğru çıktıyı elle hesaplar. Girdide iki abone, üçer dönem, bir yinelenen satır ve zorunlu alanı eksik bir satır vardır; dolayısıyla üç aşama da işini yapmak zorundadır. Yanına aynı hesabı yapan ama sonucu bir önceki dönemin etiketiyle yazan bir kural konur.

def donustur(kayit):
    """Ham satirdan tuketim satirina: ayikla , tekille , donem farki."""
    v, _ = a1_ayikla(kayit)
    v, _ = a2_tekille(v)
    v, _ = a3_tuketim(v)
    return v


def donustur_kaydirmali(kayit):
    """AYNI hesap , tek fark: sonuc bir onceki donemin etiketiyle yaziliyor."""
    return [dict(k, donem=DONEM[DONEM.index(k["donem"]) - 1]) for k in donustur(kayit)]


def satir(ab, d, e, tip="mesken"):
    return {"abone": ab, "donem": d, "bolge": "dogu", "tip": tip, "endeks": e, "birim": "m3"}


# Sabit girdi: sekiz satir , icinde bir yinelenen ve bir eksik alanli satir var.
SABIT = [satir("T1", "2026-01", 100), satir("T1", "2026-02", 112),
         satir("T1", "2026-02", 112), satir("T1", "2026-03", 130),
         satir("T2", "2026-01", 500, "isyeri"), satir("T2", "2026-02", 540, "isyeri"),
         satir("T2", "2026-03", 545, "isyeri"), {"abone": "T3", "donem": "2026-02"}]
# Elle hesaplanmis dogru cikti: testin kahini.
BEKLENEN = [("T1", "2026-02", 12), ("T1", "2026-03", 18),
            ("T2", "2026-02", 40), ("T2", "2026-03", 5)]


def ozu(v):
    return sorted((k["abone"], k["donem"], k["m3"]) for k in v)


def yaz(ad, v):
    print(f"{ad:<11}", " ".join(f"{a}/{d[-2:]}={m}" for a, d, m in ozu(v)),
          " gecti:", ozu(v) == sorted(BEKLENEN))


print("sabit girdi:", len(SABIT), "satir , beklenen cikti:", len(BEKLENEN), "satir")
yaz("dogru", donustur(SABIT))
yaz("kaydirmali", donustur_kaydirmali(SABIT))
print("ayni girdi iki kez kosuldu , cikti ayni mi:",
      ozu(donustur(SABIT)) == ozu(donustur(SABIT)))
sabit girdi: 8 satir , beklenen cikti: 4 satir
dogru       T1/02=12 T1/03=18 T2/02=40 T2/03=5  gecti: True
kaydirmali  T1/01=12 T1/02=18 T2/01=40 T2/02=5  gecti: False
ayni girdi iki kez kosuldu , cikti ayni mi: True

Sekiz satır dörde iniyor: yinelenen satır tekille aşamasında, eksik alanlı satır ayikla aşamasında, her abonenin ilk dönemi ise tüketimin tanımı gereği düşüyor. Dört sayının tamamı elle doğrulanabilir: T1 için 112 − 100 = 12 ve 130 − 112 = 18, T2 için 540 − 500 = 40 ve 545 − 540 = 5.

Kaydırmalı kural aynı dört sayıyı üretiyor ve testte hemen düşüyor, çünkü etiketler bir dönem geriye kaymış. Testin gücü buradadır: hesabın kendisi doğru, yanlış olan yalnızca sonucun hangi döneme yazıldığıdır, ve bu ayrım sabit girdide gözle görülür. Son satır kursun kuralını sabit girdi üzerinde doğrular — aynı girdi iki kez koşturulduğunda çıktı birebir aynıdır.

Çıktıya Konan Beklentiler

Üçüncü blok altı beklenti tanımlar. Hepsi dönüşümün kendi çıktısına bakar; hiçbiri kaynağa dönmez ve hiçbiri önceden bilinen bir doğru sonuca ihtiyaç duymaz. Beklentiler altı bozulma sınıfı üzerinde tek tek koşturulur.

import statistics

# Beklentilerin hepsi donusumun KENDI ciktisina bakar , kaynaga degil.
SATIR, HUCRE, TAVAN = 2200, 55, 200


def b_anahtar_tekil(v, oz):
    ad = [(k["abone"], k["donem"]) for k in v]
    return len(ad) != len(set(ad))


def b_negatif_yok(v, oz):
    return any(k["m3"] < 0 for k in v)


def b_tavan(v, oz):
    return any(k["m3"] > TAVAN for k in v)


def b_satir_orani(v, oz):
    return abs(len(v) - SATIR) / SATIR > 0.02


def b_hucre_tamligi(v, oz):
    return len(oz) != HUCRE


def b_oynaklik(v, oz):
    """Bir bolgenin donem hucreleri kendi ortancasindan cok uzaklasmamali."""
    for b in BOLGE:
        d = [x for (bb, _), x in oz.items() if bb == b]
        orta = statistics.median(d) if d else 0
        if not d or any(abs(x - orta) / abs(orta) > 0.30 for x in d):
            return True
    return False


BEKLENTI = [("anahtar_tekil", b_anahtar_tekil), ("negatif_yok", b_negatif_yok),
            ("tavan", b_tavan), ("satir_orani", b_satir_orani),
            ("hucre_tamligi", b_hucre_tamligi), ("oynaklik", b_oynaklik)]


def kosut(v):
    """Bozulan beklentilerin adi , ozet ve sunum."""
    oz = a4_ozet(v)[0]
    return [a for a, f in BEKLENTI if f(v, oz)], oz, a5_sun(oz)[0]


KAYNAK = kaynak_uret()
TEMIZ, _, SUNUM = kosut(donustur(KAYNAK))
print("beklenti:", len(BEKLENTI), " saglam hatta bozulan:", len(TEMIZ))
print(f"{'bozulma':<15}{'satir':>6}{'hucre':>7}{'bozulan beklenti':>34}{'sunum':>9}")
for s in BOZULMA:
    v = donustur(boz(KAYNAK, s)[0])
    bz, oz, sn = kosut(v)
    print(f"{s:<15}{len(v):>6}{len(oz):>7}{','.join(bz) or 'yok':>34}"
          f"{'ayni' if sn == SUNUM else 'degisti':>9}")
beklenti: 6  saglam hatta bozulan: 0
bozulma         satir  hucre                  bozulan beklenti    sunum
gec_gelen        2120     55              satir_orani,oynaklik  degisti
yinelenen        2200     55                               yok     ayni
sema_degisti     1800     45         satir_orani,hucre_tamligi  degisti
birim_degisti    2200     55        negatif_yok,tavan,oynaklik     ayni
eksik_bolum      2108     53         satir_orani,hucre_tamligi  degisti
kayan_tip        2200     55                               yok     ayni

İlk satır bedelin olmadığını gösterir: sağlam hatta altı beklentinin hiçbiri ateşlemiyor, yani iyi satır yanlış engellenmiyor. Altı beklenti altı sınıfın dördünü yakalıyor.

birim_degisti satırı bu dersin en önemli sayısıdır. Veri Mühendisliğine Giriş kursunda aynı bozulma yalnızca dördüncü aşamanın özetine bakan dağılım denetimiyle görünüyordu; burada negatif_yok ve tavan beklentileri onu dönüşümün çıktısında, üçüncü aşamada yakalıyor. Aradaki fark yalnız bir aşama değildir: dağılım denetimi çalışabilmek için sağlam bir taban özetine ihtiyaç duyar, bu üç beklenti hiçbir tabana ihtiyaç duymaz. Yeni kurulmuş bir hatta karşılaştırılacak bir geçmiş yoktur, değişmez ise ilk koşumda yazılabilir. Sunum sütunu ise birim_degisti için hâlâ ayni diyor — sunulan beş değer birebir yerinde durduğu hâlde iki dönem çöptür ve bunu söyleyen tek şey beklentidir.

Kalan üç yakalama sıradandır: gec_gelen ile eksik_bolum satır sayısını düşürüp hücre bandını kırıyor, sema_degisti ise dönemin tamamını ayikla aşamasında kaybettiriyor ve 1800 satırla 45 hücre bırakıyor.

Hiçbir Beklentinin Görmediği Kalıntı

Tabloda iki satır boş: yinelenen ve kayan_tip. Dördüncü blok bunların yanına iki kural yazar — biri hatalı, biri kasıtlı — ve aynı altı beklentiyi onlar üzerinde koşturur.

KAYDIRMALI = donustur_kaydirmali(KAYNAK)
KATSAYILI = [dict(k, m3=k["m3"] * (1.05 if k["tip"] == "isyeri" else 1.0))
             for k in donustur(KAYNAK)]
for ad, v in (("kaydirmali kural", KAYDIRMALI), ("katsayili kural", KATSAYILI)):
    bz, oz, sn = kosut(v)
    print(f"{ad:<17} satir={len(v)} hucre={len(oz)} bozulan={','.join(bz) or 'yok'}")
    print("  sunum:", sn)
print("  taban:", SUNUM)
kaydirmali kural  satir=2200 hucre=55 bozulan=yok
  sunum: {'merkez': 1321.45, 'kuzey': 1212.55, 'dogu': 881.36, 'bati': 833.09, 'guney': 1065.55}
katsayili kural   satir=2200 hucre=55 bozulan=yok
  sunum: {'merkez': 1357.32, 'kuzey': 1235.57, 'dogu': 902.13, 'bati': 859.22, 'guney': 1095.81}
  taban: {'merkez': 1321.45, 'kuzey': 1212.55, 'dogu': 881.36, 'bati': 833.09, 'guney': 1065.55}

Kalıntı üç parçadır ve üçünün mekanizması ayrıdır.

Dönüşümün yuttuğu iki sınıf. yinelenen tekille aşamasında, kayan_tip ise tür çevriminde düzeliyor; çıktıya bakan bir beklenti onarılmış bir bozulmayla hiç olmamış bir bozulmayı birbirinden ayıramaz. Bu iki satırın boş kalması dönüşümün sağlamlığının kanıtıdır, ama üreticinin hâlâ metin gönderdiğini kimse öğrenmez ve gelecek dönemin metni sayıya çevrilemeyebilir. Çıktıya konan beklenti, girdiye konan denetimin yerini tutmaz; ikisi ayrı sorulara bakar.

Etiket kaydıran kural. Kaydırmalı kural 2200 satır, 55 hücre üretiyor, hiçbir beklentiyi bozmuyor ve sunumu birebir aynı bırakıyor. Nedeni yapısaldır: hesaplanan sayılar kümesi hiç değişmemiş, yalnızca hangi döneme yazıldıkları kaymıştır; bölge başına on bir hücrenin ortalaması aynı sayıların ortalamasıdır. Çıktının kendisi hakkında yazılabilecek hiçbir değişmez bunu göremez, çünkü çıktının kendisi kusursuzdur — yanlış olan hangi gerçeğe karşılık geldiğidir. Bu hatayı yakalayan tek şey dersin başındaki sabit girdili testtir.

Kasıtlı kural değişikliği. Katsayılı kural işyeri tüketimini 1,05 ile raporluyor. Altı beklentinin hiçbiri bozulmuyor, çünkü ortada bozulma yok; sunum ise beş değerin hepsinde yukarı kayıyor. Beklentiler bu farkın istenen bir değişiklik mi yoksa kaza mı olduğunu söyleyemez. Ayrımı yapan tek şey, kural sürümünün kayda yazılmasıdır.

Özet

  • Bir dönüşüm ancak saf fonksiyonse sınanabilir; aynı girdiye her koşumda aynı çıktıyı vermeyen bir kuralın sınanacak sonucu yoktur.
  • Birim testi kuralın hatasını, beklenti verinin bozulmasını yakalar; ayrım kâhinin nereden geldiğindedir ve ikisi birbirinin yerine geçmez.
  • Sağlam hatta altı beklentinin hiçbiri ateşlemiyor ve altı bozulma sınıfının dördünü yakalıyorlar; yinelenen ile kayan_tip dönüşümün kendisi tarafından yutulduğu için çıktıda hiç görünmüyor.
  • birim_degisti çıktıya konan beklentiyle üçüncü aşamada ve hiçbir taban çizgisi olmadan yakalanıyor, oysa sunulan beş değer birebir aynı kalıyor.
  • Dönem etiketini bir dönem geriye kaydıran kural 2200 satır, 55 hücre ve aynı beş sunum değerini ürettiği için altı beklentinin hiçbirine takılmıyor; onu yalnız sabit girdili birim testi görüyor.

Sonraki Adım

Son bloğun katsayılı kuralı bu dersin kapatamadığı soruyu açıyor. Kural değişti ve yeni koşumlar doğru sonucu üretecek — ama on iki dönemin on birisi eski kuralla hesaplanmış olarak duruyor. Bütün dönemleri yeniden hesaplamak koşum bedeli demektir, yalnız birkaç dönemi yeniden hesaplamak ise raporun bir kısmını yeni, kalanını eski kuralla bırakır. Sonraki ders geri doldurmayı bu iki seçenek üzerinden ölçer: kaç dönemin yeniden hesaplandığını, karışık raporun tam uygulamadan ne kadar saptığını ve o raporun hangi denetimlerden geçtiğini sayar.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat