Veri kalitesi skor kartı: altı boyutta ölçüm ve zaman içinde takip
Veri kalitesini altı boyutta pandas ile ölçün: doluluk, benzersizlik, geçerlilik, tutarlılık, güncellik, doğruluk. Metrikleri tarihli saklayıp eşikle izleyin.
Veri kalitesi skor kartı, "bu veri iyi mi kötü mü" sorusunu bir his olmaktan çıkarıp her koşumda hesaplanan, tarihli olarak saklanan ve zaman içinde karşılaştırılan bir sayı setine dönüştürür. Danışmanlık işlerinde en sık karşılaştığım tablo şu: bir rapor yanlış çıkıyor, ekip veriyi suçluyor, ama kimse "hangi kolonun ne kadarı boş" ya da "geçen haftaya göre ne değişti" sorusuna cevap veremiyor. Ölçüm olmayınca tartışma da bitmiyor.
Bu yazıda veri kalitesini altı boyutta tanımlıyorum: doluluk, benzersizlik, geçerlilik, tutarlılık, güncellik ve doğruluk. Her biri için "0 ile 1 arasında bir oran nasıl hesaplanır" sorusuna somut bir cevap veriyorum; ardından bu oranları pandas ile hesaplayan bir fonksiyon ve sonuçları tarihli olarak SQLite'a yazıp önceki koşumlarla karşılaştıran küçük bir depo gösteriyorum.
Amaç büyük bir veri kalitesi platformu kurmak değil. Amaç, her gece çalışan boru hattının sonuna bir adım eklemek: ölç, kaydet, eşikle kıyasla, gerçekten bozulduysa haber ver. Aynı yaklaşımı SQLite ile kurduğum küçük boru hatlarında uzun süredir bu biçimde kullanıyorum; ölçüm katmanı, boru hattının kendisinden daha karmaşık olmak zorunda değil.
Altı boyut ve ölçülebilir tanımları
Boyutların adı sektörde az çok ortak; asıl iş her birini bir formüle bağlamak. Benim kullandığım tanımlar şunlar:
- Doluluk: zorunlu kolonlardaki boş olmayan hücre sayısının toplam hücre sayısına oranı. Hangi kolonların zorunlu olduğunu önceden yazmadan doluluk ölçülmez; "bütün kolonlar" demek boyutu anlamsızlaştırır.
- Benzersizlik: iş anahtarına göre (müşteri numarası, sipariş numarası) tekil satırların toplam satıra oranı. Teknik birincil anahtar zaten tekildir; ölçülmesi gereken iş anahtarıdır.
- Geçerlilik: dolu değerler arasında alan kuralına uyanların oranı. E-posta biçimi, negatif olamayacak tutar, izin verilen kod listesi. Boş değer burada sayılmaz; o doluluğun konusu.
- Tutarlılık: kolonlar ya da tablolar arası çapraz kurallara uyan satır oranı. "İptal durumundaki siparişin tutarı sıfır olmalı" gibi.
- Güncellik: belirlenen pencere içinde güncellenmiş kayıt oranı ya da en yeni kaydın yaşı. Hangisini seçtiğinizi belgeleyin; ikisi farklı şeyler söyler.
- Doğruluk: güvenilir bir referans kaynağıyla eşleşen kayıt oranı. Referans yoksa doğruluk ölçülemez; bunu boş bırakmak, uydurmaktan iyidir.
Altı boyutu tek bir "kalite puanı"na indirgemekten kaçınıyorum. Ortalama almak, doluluğu yüksek ama doğruluğu düşük bir tabloyu "iyi" gösterir. Skor kartı altı ayrı satır olarak kalmalı; karar verecek kişi hangi satırın kırmızı olduğunu görmeli.
pandas ile boyutları hesaplayan ölçüm fonksiyonu
Aşağıdaki fonksiyon bir müşteri tablosu için altı boyutu hesaplıyor. Kuralları okunabilirlik için fonksiyonun içine gömdüm; gerçek kullanımda kuralları bir sözlükten alıp aynı fonksiyonu birden çok tabloya uygulamak daha temiz.
import pandas as pd
EPOSTA = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
ZORUNLU = ["musteri_id", "eposta", "sehir", "tutar"]
def olc(df, anahtar, zaman_kolonu, referans=None, pencere_saat=24):
"""Altı boyutu 0-1 aralığında oran olarak döndürür."""
m = {}
# Doluluk: zorunlu kolonlardaki dolu hücre oranı
m["doluluk"] = float(df[ZORUNLU].notna().to_numpy().mean())
# Benzersizlik: iş anahtarına göre tekil satır oranı
m["benzersizlik"] = float(1 - df.duplicated(subset=anahtar).mean())
# Geçerlilik: yalnızca DOLU değerler arasında kurala uyan oran
kurallar = pd.concat([
df["eposta"].dropna().str.match(EPOSTA),
df["tutar"].dropna().ge(0),
])
m["gecerlilik"] = float(kurallar.mean()) if len(kurallar) else 1.0
# Tutarlılık: çapraz kural - iptal edilen kaydın tutarı sıfır olmalı
ihlal = (df["durum"] == "iptal") & (df["tutar"] > 0)
m["tutarlilik"] = float(1 - ihlal.mean())
# Güncellik: pencere içinde güncellenmiş kayıt oranı (UTC'ye sabitlenir)
yas = pd.Timestamp.now(tz="UTC") - pd.to_datetime(df[zaman_kolonu], utc=True)
m["guncellik"] = float((yas <= pd.Timedelta(hours=pencere_saat)).mean())
# Doğruluk: referans kaynakla eşleşen kayıt oranı; referans yoksa ölçülmez
if referans is None:
m["dogruluk"] = None
else:
es = df.merge(referans, on=anahtar, suffixes=("", "_ref"))
m["dogruluk"] = float((es["sehir"] == es["sehir_ref"]).mean()) if len(es) else 0.0
return m
Dikkat ettiğim birkaç nokta:
- Geçerlilik yalnızca dolu değerler üzerinden hesaplanıyor. Aksi hâlde boş hücreler iki boyutu birden düşürür ve hangi sorunun neye ait olduğu belirsizleşir.
- Güncellikte zaman dilimi açıkça UTC'ye çekiliyor. Sunucu saati ile veri kaynağının saati farklı dilimdeyse güncellik oranı sebepsiz yere düşer; bunu bir kez yaşadıktan sonra her ölçümde
utc=Trueyazıyorum. - Referans yoksa doğruluk
Nonedönüyor. Sıfır yazmak "veri tamamen yanlış" demektir; ölçülmemiş ile sıfır ölçülmüş aynı şey değildir. - Her oran
floatolarak dönüyor; numpy tipleri SQLite'a yazarken sorun çıkarabildiği için dönüşümü fonksiyonun içinde yapıyorum.
Metrikleri tarihli saklayan küçük depo
Tek bir ölçüm hiçbir şey söylemez. Bugünkü doluluk oranı iyi mi kötü mü, geçen ayki değeri bilmeden karar veremezsiniz. Bu yüzden her koşumun sonucunu uzun formatta (zaman, kaynak, boyut, değer) bir SQLite tablosuna yazıyorum. Yeni boyut ya da yeni kaynak eklemek şema değişikliği gerektirmiyor; aynı dosyada onlarca tabloyu izleyebilirsiniz.
import sqlite3
from datetime import datetime, timezone
class MetrikDeposu:
def __init__(self, yol="veri_kalitesi.db"):
self.con = sqlite3.connect(yol)
self.con.execute("""
CREATE TABLE IF NOT EXISTS metrik (
zaman TEXT NOT NULL,
kaynak TEXT NOT NULL,
boyut TEXT NOT NULL,
deger REAL,
PRIMARY KEY (zaman, kaynak, boyut)
)""")
def kaydet(self, kaynak, metrikler, zaman=None):
zaman = zaman or datetime.now(timezone.utc).isoformat(timespec="seconds")
self.con.executemany(
"INSERT INTO metrik VALUES (?, ?, ?, ?)",
[(zaman, kaynak, boyut, deger) for boyut, deger in metrikler.items()],
)
self.con.commit()
return zaman
def son(self, kaynak, boyut, n=7):
"""Son n koşumun değerlerini eskiden yeniye döndürür."""
satirlar = self.con.execute(
"SELECT deger FROM metrik WHERE kaynak=? AND boyut=? "
"ORDER BY zaman DESC LIMIT ?", (kaynak, boyut, n)).fetchall()
return [s[0] for s in reversed(satirlar)]
def degerlendir(self, kaynak, esikler, tolerans=2):
"""Boyut başına durum: NORMAL / DALGALANMA / BOZULMA / VERI_YOK."""
sonuc = {}
for boyut, esik in esikler.items():
seri = [d for d in self.son(kaynak, boyut, n=tolerans) if d is not None]
if not seri:
sonuc[boyut] = "VERI_YOK"
continue
ihlal = [d < esik for d in seri]
if not ihlal[-1]:
sonuc[boyut] = "NORMAL"
elif len(ihlal) >= tolerans and all(ihlal):
sonuc[boyut] = "BOZULMA"
else:
sonuc[boyut] = "DALGALANMA"
return sonuc
Depo, her boyut için son koşumları eşikle kıyaslayıp dört durumdan birini döndürüyor. Boru hattının sonundaki kullanımı şu kadar:
depo = MetrikDeposu("veri_kalitesi.db")
metrikler = olc(df, anahtar="musteri_id", zaman_kolonu="guncelleme", referans=ref_df)
depo.kaydet("crm_musteri", metrikler)
ESIK = {"doluluk": 0.98, "benzersizlik": 0.999, "gecerlilik": 0.97,
"tutarlilik": 0.99, "guncellik": 0.90, "dogruluk": 0.95}
durum = depo.degerlendir("crm_musteri", ESIK, tolerans=2)
print(durum) # ör. {'doluluk': 'NORMAL', 'guncellik': 'DALGALANMA', ...}
kaydet metodunun zaman parametresi bilinçli olarak dışarıdan verilebiliyor: geçmiş bir günün verisini yeniden işlediğinizde ölçümün de o günün tarihiyle yazılması gerekir, yoksa trend grafiği yanlış konuşur. Tablonun birincil anahtarı (zaman, kaynak, boyut) üçlüsü; aynı koşum yanlışlıkla iki kez yazılırsa sessizce çift satır oluşmak yerine hata alırsınız. Ben sessiz çift kaydı, gürültülü hataya tercih etmiyorum.
Eşik, uyarı ve dalgalanmayı bozulmadan ayırmak
Uyarı sisteminin en büyük düşmanı yanlış alarmdır. Birkaç kez boşuna rahatsız edilen ekip sonraki uyarıyı okumaz. Bu yüzden eşik aşımını doğrudan alarma bağlamıyorum; degerlendir içindeki ayrım tam da bunun için var:
- DALGALANMA: yalnızca son koşum eşiğin altında. Kaynak sistemde geç kapanan bir gece işi, yarım kalan bir aktarım ya da tatil günü. Kayda geçer, alarm üretmez.
- BOZULMA: art arda
toleranskadar koşum eşiğin altında. Bu artık bir olay değil, bir durum. Uyarı burada üretilir. - NORMAL: son koşum eşiğin üstünde. Önceki koşumda ihlal olsa bile kendi kendine düzelmiş demektir; bunu ayrıca not etmek faydalı, çünkü kendi kendine düzelen sorunlar çoğunlukla bir zamanlama hatasına işaret eder.
- VERI_YOK: o boyut için hiç kayıt yok. Ölçüm adımı hiç çalışmamış olabilir; bu, "her şey yolunda" ile karıştırılmaması gereken ayrı bir sinyaldir.
Sabit eşik her boyuta uymaz. Güncellik gibi hafta sonu doğal olarak düşen bir metrikte sabit eşik her pazartesi yanlış alarm verir. Bu durumda eşiği son yedi koşumun medyanının belirli bir pay altına koymak daha sağlıklı; son() metodu bunun için gereken veriyi zaten döndürüyor, hesabı degerlendir içine eklemek birkaç satır.
Eşik değerini kimin belirleyeceği sorusunun cevabı teknik ekip değil, veriyi kullanan taraf olmalı. Ben genellikle ilk haftaları eşiksiz, yalnızca kaydederek geçiriyorum; ardından gerçek dağılıma bakıp eşiği birlikte koyuyoruz. Aynı "önce kalibre et, sonra alarm kur" ilkesini Excel tabanlı kalite kontrol otomasyonunda da uyguluyorum; araç farklı, disiplin aynı.
Koşum düzeni ve skor kartının sunumu
Ölçüm, boru hattının son adımı olarak çalışmalı; bağımsız bir zamanlanmış iş olarak değil. Aksi hâlde aktarım gecikince ölçüm eski veriyi ölçer ve "her şey normal" der. Uyguladığım düzen şu:
- Aktarım biter, aynı süreç içinde
olc()çağrılır, sonuçkaydet()ile yazılır. degerlendir()çıktısında en az bir BOZULMA varsa bildirim gider; DALGALANMA yalnızca günlük özete girer.- Ölçüm adımının kendisi hata verirse bu da ayrı bir olaydır. Depoda o gün için satır yoksa bunu yakalayan bir kontrol olmalı; sessiz başarısızlık, yanlış alarmdan daha tehlikelidir.
- Skor kartı görünümü, metrik tablosundan haftalık bir pivot: satırlar boyut, sütunlar gün. Eşiğin altındaki hücre işaretlenir, trend tek bakışta okunur.
Bu görünümü ayrı bir araçla üretmeye gerek yok; aynı SQLite dosyasını okuyan birkaç satırlık bir pandas pivotu ya da mevcut raporlama aracınız yeterli. Depo tek bir dosya olduğu için yedeklemesi de kolay: dosyayı mevcut yedek setine eklemek yeterli. Böylece aylar sonra "bu tablo hep böyle miydi" sorusunun cevabı tahmin değil, kayıt olur.
Veri kalitesi skor kartının değeri tek bir ölçümden değil, aynı ölçümün her gün aynı biçimde tekrarlanmasından gelir. Altı boyut, bir pandas fonksiyonu, bir SQLite tablosu ve toleranslı bir eşik mantığı; bu kadarı çoğu kurum içi tabloyu "hissettiğimiz" hâlden "bildiğimiz" hâle getirmeye yetiyor.
Kendi veri boru hattınıza böyle bir ölçüm katmanı eklemek ya da mevcut kalite raporlarınızı gözden geçirmek isterseniz ana sayfadan ne yaptığıma bakabilir, iletişim bölümünden bana yazabilirsiniz.