Tüm yazılar
Veri Otomasyonu 6 dk okuma

Company Matching & Data Normalization: Fuzzy Matching ile Veri Temizleme

Farklı kaynaklardan gelen şirket kayıtlarını fuzzy matching ile eşleştirme: normalizasyon, blocking, RapidFuzz skorlama ve eşik seçimiyle veri temizleme.

#python #fuzzy-matching #data-cleaning #rapidfuzz #csv

Veri danışmanlığı projelerimde en sık karşıma çıkan sorunlardan biri, farklı kaynaklardan gelen şirket kayıtlarının birbirini tutmaması. CRM'de "Acme Teknoloji A.Ş." yazıyor, muhasebe yazılımında "ACME TEKNOLOJI AS", satış ekibinin Excel dosyasında ise "Acme Tech". Üçü de aynı şirket; ama birebir karşılaştırma yapan her sistem bunları üç ayrı müşteri olarak sayıyor. Bu yazıda, bu sorunu fuzzy matching (bulanık eşleştirme) ile nasıl çözdüğümü, çalışan Python örnekleriyle adım adım anlatacağım.

Bu tür kayıt eşleştirme işleri genellikle tek başına yapılmaz; çok kaynaklı bir veri zenginleştirme pipeline'ının ortasındaki kritik halkadır. Veriyi topladıktan sonra, zenginleştirip raporlamadan önce hangi kaydın hangi kayıtla aynı varlığı temsil ettiğini çözmeniz gerekir. Zincir hep aynıdır: önce normalizasyon, sonra blocking ile aday havuzunu daraltma, sonra benzerlik skorlaması, eşik seçimi ve en sonunda denetlenebilir bir birleştirme.

Deneyimimde en önemli ders şu oldu: fuzzy matching sihirli bir değnek değil. Kirli veri üzerinde doğrudan benzerlik skoru hesaplarsanız hem yavaş hem güvenilmez sonuç alırsınız. İşin yarısı, skorlamadan önceki hazırlıktır.

Neden birebir eşleşme yetmez?

Aynı şirketin farklı kaynaklarda farklı görünmesinin tipik nedenleri şunlar:

  • Büyük-küçük harf ve Türkçe karakter farkları: "İstanbul Lojistik" / "ISTANBUL LOJISTIK".
  • Hukuki ekler: A.Ş., Ltd. Şti., GmbH, Inc., LLC — bazen var, bazen yok, bazen farklı kısaltılmış.
  • Noktalama ve boşluk farkları: "A.B.C. Yazılım" / "ABC Yazilim".
  • Marka adı ile resmi unvan ayrımı: tabelada "Acme Tech", ticaret sicilinde "Acme Teknoloji Anonim Şirketi".
  • Kelime sırası: "Değirmenci Veri Danışmanlık" / "Veri Danışmanlık Değirmenci".
  • Yazım hataları: elle girilen her alanda kaçınılmaz.

Excel'deki DÜŞEYARA veya SQL'deki birebir JOIN bu varyasyonların hiçbirini yakalayamaz. Veri temizleme sürecinin ilk görevi, bu farkların kuralla çözülebilen kısmını deterministik olarak ortadan kaldırmaktır; fuzzy matching'i yalnızca kuralla çözülemeyen kısma saklarım.

Adım 1: Normalizasyon ve şirket adı temizliği

Normalizasyonun amacı, iki farklı yazımı karşılaştırmadan önce ortak bir biçime indirgemek. Şirket adı normalizasyonu için tipik adımlar: küçük harfe çevirme, Türkçe karakterleri ASCII karşılıklarına indirgeme, noktalama temizliği, hukuki eklerin (A.Ş., Ltd. Şti., GmbH, Inc.) silinmesi ve fazla boşlukların tekilleştirilmesi. Bu temizlik adımlarının omurgası düzenli ifadelerdir; log analizi yazımda anlattığım regex desenleri burada da aynen iş görür.

Türkçe veri setlerinde klasik bir tuzak var: İ/I dönüşümü. Python'un lower() metodu Unicode'un varsayılan kurallarıyla çalışır. "İ".lower() çağrısı tek bir "i" değil, "i" artı birleşen nokta (U+0307) olmak üzere iki kod noktası üretir; "I".lower() ise Türkçe'de beklediğiniz "ı" yerine "i" döndürür. Sonuç: "İSTANBUL" ve "istanbul" görünüşte aynı olsa da bayt düzeyinde eşleşmeyebilir, skorlar sessizce düşer. Çözümüm, lower() çağrısından önce Türkçe harfleri elle dönüştürmek:

import re
import unicodedata

TR_DONUSUM = str.maketrans("İIıŞşĞğÇçÖöÜü", "iiissggccoouu")

SON_EKLER = re.compile(
    r"\s+(a\s?s|anonim sirketi|ltd(\s+sti)?|limited sirketi|sti|"
    r"gmbh|inc|corp|co|llc|plc|sa)$"
)

def normalize(ad: str) -> str:
    ad = ad.translate(TR_DONUSUM).lower()
    ad = unicodedata.normalize("NFKD", ad)
    ad = "".join(k for k in ad if not unicodedata.combining(k))
    ad = re.sub(r"[^a-z0-9 ]+", " ", ad)
    ad = re.sub(r"\s+", " ", ad).strip()
    onceki = None
    while onceki != ad:          # üst üste binmiş ekleri de temizle
        onceki = ad
        ad = SON_EKLER.sub("", ad)
    return ad

print(normalize("Acme Teknoloji A.Ş."))       # acme teknoloji
print(normalize("ACME TEKNOLOJI AS"))         # acme teknoloji
print(normalize("Acme Teknoloji Ltd. Şti."))  # acme teknoloji

Üç farklı yazım da "acme teknoloji" biçimine indi; bu üçlü artık fuzzy skora bile gerek kalmadan eşleşir. Hukuki ek listesini veri setinize göre genişletmeniz gerekir — uluslararası veride S.A., B.V., Pty gibi ekler de gelir. Dikkat edilecek nokta: ekleri sildiğiniz için "Acme A.Ş." ile "Acme Ltd." aynılaşır; bunlar bazen aynı grubun farklı tüzel kişilikleridir. Bu ayrım sizin için önemliyse eki silmek yerine ayrı bir alana taşıyıp eşleştirme sonrası kontrolde kullanın.

Adım 2: Blocking ile aday havuzunu daraltma

Naif yaklaşım her kaydı her kayıtla karşılaştırmaktır; bu N² büyür. 50.000 kayıtlık bir listede yaklaşık 1,25 milyar çift söz konusudur — RapidFuzz ne kadar hızlı olursa olsun bu ölçek pratikte savunulamaz. Blocking (bloklama), yalnızca aynı bloğa düşen kayıtları karşılaştırarak bu patlamayı önler. Kullandığım tipik blok anahtarları:

  • Normalize adın ilk kelimesi veya ilk birkaç harfi,
  • Varsa şehir/ülke gibi yapısal alanlar,
  • Vergi numarası, web sitesi domain'i gibi güçlü alanlarla önceden kesinleşen eşleşmeleri havuzdan tamamen çıkarmak,
  • Fonetik anahtarlar (Soundex/Metaphone) — İngilizce için tasarlandıklarından Türkçe'de sınırlı fayda sağlarlar.

Buradaki denge şudur: blok çok dar olursa gerçek eşleşmeleri kaçırırsınız (ilk kelimesi farklı yazılmış iki kayıt farklı bloklara düşer), çok geniş olursa N² geri gelir. Deneyimimde tek mükemmel anahtar aramak yerine iki üç farklı anahtarla ayrı ayrı bloklayıp aday çiftlerin birleşimini almak daha sağlıklı sonuç veriyor.

Adım 3: RapidFuzz ile benzerlik skoru

Skorlama için tercihim RapidFuzz: Levenshtein tabanlı benzerlik ölçülerini C++ hızında hesaplayan, eski fuzzywuzzy'nin yerini almış bir kütüphane. Tüm skorlar 0-100 aralığındadır, ama hangi fonksiyonu seçtiğiniz sonucu ciddi değiştirir:

  • fuzz.ratio: iki metni bütün olarak karşılaştırır; kelime sırası ve fazladan kelimeler skoru düşürür. En katı ölçüdür.
  • fuzz.partial_ratio: kısa metnin, uzun metnin içindeki en benzer parçasını arar. "acme" ile "acme teknoloji" yüksek skor alır — kısaltmalar için faydalı, ama yanlış pozitife de en yatkın ölçü.
  • fuzz.token_sort_ratio: kelimeleri alfabetik sıralayıp karşılaştırır; kelime sırası farklarını tamamen yok sayar.
  • fuzz.token_set_ratio: ortak kelime çekirdeğine odaklanır; bir tarafta fazladan kelime olsa bile çekirdek aynıysa yüksek skor verir. Şirket adlarında benim varsayılan başlangıç noktam budur.

Blocking ve skorlamayı birleştiren iskelet şöyle görünüyor:

from collections import defaultdict
from rapidfuzz import fuzz

def blok_anahtari(norm_ad: str) -> str:
    return norm_ad.split(" ")[0][:4] if norm_ad else ""

bloklar = defaultdict(list)
for kayit in kayitlar:          # her kayit: {"id": ..., "norm": normalize(ad)}
    bloklar[blok_anahtari(kayit["norm"])].append(kayit)

otomatik, inceleme = [], []
for grup in bloklar.values():
    for i in range(len(grup)):
        for j in range(i + 1, len(grup)):
            skor = fuzz.token_set_ratio(grup[i]["norm"], grup[j]["norm"])
            if skor >= 92:
                otomatik.append((grup[i]["id"], grup[j]["id"], skor))
            elif skor >= 80:
                inceleme.append((grup[i]["id"], grup[j]["id"], skor))

Koddaki 92 ve 80 değerleri örnek; asıl işin bu sayıları kendi verinize göre kalibre etmek olduğunu bir sonraki bölümde anlatıyorum.

Adım 4: Eşik seçimi ve insan faktörü

Her veri seti için geçerli tek bir doğru eşik yok. Benim yöntemim: aday çiftlerden rastgele bir örneklem alıp elle "aynı / farklı" diye etiketlemek, sonra hataların hangi skor aralığında yoğunlaştığına bakmak. Pratikte üç bant kullanıyorum:

  • Yüksek bant: otomatik kabul — normalizasyon iyi yapılmışsa buradaki hatalar nadirdir.
  • Orta bant: insan incelemesine giden kuyruk. Karar verilemeyen çiftleri sessizce birleştirmek yerine bir insana göstermek, deneyimimde her zaman daha ucuza çıkıyor.
  • Alt bant: otomatik ret.

Kritik uyarı: yüksek skor, aynı şirket demek değildir. "Acme Holding" ile "Acme Teknoloji" token_set_ratio'da pekâlâ yüksek skor alabilir; oysa bunlar aynı grubun ayrı tüzel kişilikleri olabilir. Bu tür sistematik yanlış pozitifleri eşikle değil, kural katmanıyla (holding/grup kelimelerini ayırt eden kontroller) yakalamak gerekir. İnceleme kuyruğunu küçük tutmanın yolu da eşikle oynamak değil, normalizasyonu ve blocking'i iyileştirmektir.

Adım 5: Birleştirme ve denetlenebilirlik

Onaylanan çiftleri kümelere dönüştürdükten sonra (çiftler zincirleme bağlanır: A-B ve B-C eşleşmesi A-B-C kümesi demektir), her küme için bir canonical (temsilci) kayıt seçilir. Seçim kurallarım basit ve sıralı:

  • En güvenilir kaynaktan gelen kayıt önceliklidir (örneğin resmi sicil verisi, elle girilmiş CRM kaydını yener),
  • Eşitlikte en dolu kayıt (en az boş alan) kazanır,
  • O da eşitse en güncel kayıt seçilir.

Asıl önemli olan denetlenebilirlik: orijinal kayıtları asla silmem, üzerine yazmam. Ayrı bir eşleştirme tablosunda her satır için kaynak id, canonical id, benzerlik skoru, kullanılan yöntem (otomatik mi insan onayı mı) ve tarih tutarım. Yanlış bir birleştirme aylar sonra fark edildiğinde bu tablo sayesinde tek kaydı geri alabilirsiniz; tablo yoksa tüm çalışmayı baştan yaparsınız. Deneyimimde bir kayıt eşleştirme projesinin uzun vadeli güvenilirliği, skorlama algoritmasından çok bu kayıt disiplinine bağlı.

Sonuç

Fuzzy matching, şirket kayıtlarını birleştirmenin en görünür parçası ama tek başına yeterli değil: normalizasyon olmadan skorlar güvenilmez, blocking olmadan ölçeklenmez, eşik kalibrasyonu ve insan denetimi olmadan sonuçlara imza atamazsınız. Zinciri bütün olarak kurduğunuzda ise elle haftalar sürecek bir temizlik, büyük ölçüde otomatik ve her adımı geri alınabilir bir sürece dönüşüyor.

Bu tarz kayıt eşleştirme ve veri temizleme problemleri, veri otomasyonu çalışmalarımın önemli bir bölümünü oluşturuyor. Elinizde birleştirilmeyi bekleyen dağınık müşteri veya tedarikçi listeleri varsa iletişim bölümünden yazın; verinize birlikte bakalım.