Inhaltsverzeichnis9 Abschnitte
Dein CRM hat 847.000 Kund:innen-Records. Analytics sagt +40 % Umsatz, Finance sagt +22 %. Die Differenz sind Duplikate — dieselbe Person dreimal gezählt unter leicht unterschiedlichen Namen und Mails. Deduplication ist die Disziplin, mit der diese Zahlen konvergieren.
TL;DR
- Drei Klassen von Duplikaten brauchen drei Strategien.
- Hash-basiert: exakte Duplikate aus doppelter Ingestion — schnell und deterministisch.
- Fuzzy Matching: Tippfehler und Formatunterschiede — Schwellenwert mit Labeled Data validieren.
- Record Linkage: verschiedene Datasets ohne gemeinsamen Key — probabilistisch.
- Produktion: alle drei in einer Pipeline, plus Human Review.
Drei Ursachen, drei Strategien
| Ursache | Beispiel | Beste Methode |
|---|---|---|
| Exakte technische Duplikate | Dieselbe Zeile zweimal | Hash-basiert |
| Tippfehler, Format | "John Smith" vs "Jon Smith" | Fuzzy Matching |
| Cross-System-Entity | Person in CRM + ERP | Record Linkage |
Strategie 1 — Hash-basierte Deduplication
Das schnellste und zuverlässigste Tool für exakte Duplikate. Du berechnest einen deterministischen Hash der Unique-Felder und dedupst über den Hash.
Richtig, wenn Duplikate kommen aus:
- Doppel-Ingestion derselben Datei
- Retry-Logik ohne Idempotenz
- Mehrere Quellen schreiben dieselben Events
import pandas as pd
import hashlib
def hash_record(row: pd.Series, key_columns: list[str]) -> str:
composite_key = "|".join(
str(row[col]).strip().lower() if pd.notna(row[col]) else ""
for col in key_columns
)
return hashlib.sha256(composite_key.encode()).hexdigest()
df = pd.read_parquet("s3://raw/orders/2026/04/03/batch_*.parquet")
KEY_COLS = ["order_id", "customer_id", "placed_at", "total_cents"]
df["dedup_hash"] = df.apply(hash_record, axis=1, key_columns=KEY_COLS)
deduped = df.drop_duplicates(subset=["dedup_hash"], keep="first")
print(f"Entfernt: {len(df) - len(deduped):,} Duplikate ({(len(df) - len(deduped)) / len(df):.1%})")
In SQL (DuckDB / BigQuery / Snowflake):
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY order_id, customer_id, DATE_TRUNC('second', placed_at)
ORDER BY ingested_at ASC
) AS row_num
FROM raw.orders
)
SELECT * EXCLUDE (row_num) FROM ranked WHERE row_num = 1;
Falle: Pass auf, was im Hash-Key steckt. ingested_at oder file_path machen jede Zeile unique — du dedupst nichts.
Strategie 2 — Fuzzy Matching
Für Duplikate, die dieselbe Entity meinen, aber durch Tippfehler, Abkürzungen oder Formatunterschiede abweichen. Klassisch: Namen und Adressen.
| Algorithmus | Misst | Gut für |
|---|---|---|
| Levenshtein | Edit-Distanz | Kurze Strings, Namen |
| Jaro-Winkler | Transpositions-Gewichtung, Prefix-Bonus | Namen |
| Token Sort Ratio | Wortreihenfolge-unabhängig | Adressen, Firmen |
| Soundex / Metaphone | Phonetische Ähnlichkeit | Namen ähnlich gesprochen |
import pandas as pd
from thefuzz import fuzz
customers = pd.DataFrame({
"customer_id": ["C001", "C002", "C003", "C004", "C005"],
"name": ["John Smith", "Jon Smith", "Jonathan Smith", "Jane Doe", "J. Doe"],
"email": ["john@acme.com", "jon@acme.com", "jonathan@acme.com", "jane@acme.com", "j.doe@acme.com"],
})
def find_fuzzy_duplicates(df: pd.DataFrame, name_col: str, threshold: int = 85) -> list[tuple]:
duplicates = []
names = df[name_col].tolist()
for i in range(len(names)):
for j in range(i + 1, len(names)):
score = fuzz.token_sort_ratio(names[i], names[j])
if score >= threshold:
duplicates.append((df.index[i], df.index[j], score))
return duplicates
pairs = find_fuzzy_duplicates(customers, "name", threshold=85)
Blocking für Skalierung: Jeden Record gegen jeden anderen zu vergleichen ist O(n²) — bricht jenseits ~100k zusammen. Mit Soundex-Blocks vorfiltern:
import jellyfish
customers["soundex_block"] = customers["name"].apply(
lambda x: jellyfish.soundex(x.split()[0])
)
for block, group in customers.groupby("soundex_block"):
if len(group) > 1:
pairs = find_fuzzy_duplicates(group, "name", threshold=85)
Falle: Fuzzy Matching hat keine Ground Truth. Threshold 85 für Namen mergt vielleicht fälschlich "ACME Corp" und "ACME Labs". Immer mit Labeled Examples validieren.
Strategie 3 — Record Linkage (Entity Resolution)
Mergt Records aus verschiedenen Datasets, die dieselbe Real-World-Entity meinen — auch ohne gemeinsamen Key. Anwendungsfälle: CRM ↔ ERP, M&A-Konsolidierung, Survey ↔ Purchase-History.
import pandas as pd
import recordlinkage
crm = pd.DataFrame({
"name": ["Alice Johnson", "Bob Martinez", "Carol White"],
"email": ["alice@example.com", "bob@example.com", "carol@example.com"],
"postcode": ["10115", "10117", "10119"],
})
erp = pd.DataFrame({
"company_contact": ["A. Johnson", "Robert Martinez", "Carol White-Smith"],
"contact_email": ["alice@example.com", "b.martinez@example.com", "carol@example.com"],
"zip": ["10115", "10117", "10119"],
})
indexer = recordlinkage.Index()
indexer.block("postcode", "zip")
candidate_pairs = indexer.index(crm, erp)
comparer = recordlinkage.Compare()
comparer.string("name", "company_contact", method="jarowinkler", label="name_sim")
comparer.exact("email", "contact_email", label="email_exact")
comparer.exact("postcode", "zip", label="postcode_exact")
features = comparer.compute(candidate_pairs, crm, erp)
matches = features[features["email_exact"] == 1]
Für Large-Scale-Linkage skaliert Splink (UK Ministry of Justice) probabilistische Fellegi-Sunter-Modelle bis in hundert Millionen Records mit Spark.
Welche Strategie wann?
Duplikate aus Doppel-Ingestion / Retries?
└─ JA → Hash-basiert
Duplikate aus Tippfehlern / Abkürzungen?
└─ JA → Fuzzy Matching (mit Blocking)
Zwei Datasets ohne gemeinsamen Key?
└─ JA → Record Linkage
Mix aus allem?
└─ JA → Pipeline: erst Hash, dann Fuzzy, dann Linkage
Production-Pipeline
Raw Data
↓
[1] Hash-basiert (exakte Duplikate)
↓
[2] Standardisierung (lowercase, normalize)
↓
[3] Fuzzy Matching in Blocks
↓
[4] Human Review Queue (80–90 % Similarity)
↓
[5] Golden Record (Field-by-Field merge)
↓
Deduplicated Output
Die Human Review Queue ist bei sensiblen Daten nicht verhandelbar. 99 % Genauigkeit auf 1 Mio. Records sind immer noch 10.000 falsche Merges.
Häufige Fallen
- Threshold ohne Labeled Data: Raten statt Messen. Bau ein Sample von ~500 Paaren (echte Matches + Non-Matches) und miss Precision/Recall.
- Nicht normalisieren vor Vergleich: "ACME Inc." und "acme inc" haben Edit-Distanz > 0. Erst lowercase + Punctuation stripping.
- Falsch mergen: Beim Golden Record Field-by-Field entscheiden, welche Quelle authoritativ ist. Nicht blind den neuesten Wert nehmen.
- Temporale Duplikate ignorieren: Dasselbe Event darf legitim doppelt sein, wenn jemand zweimal gekauft hat. Timestamps + Business-Kontext nutzen.
FAQ
Wie viele Duplikate sind "normal"? Hängt vom Use Case ab. Marketing-CRMs liegen oft bei 5–15 %. Wenn dein Wert weit höher ist, liegt ein Ingestion-Problem vor.
Wann Splink, wann recordlinkage? recordlinkage für lokale Python-Workflows bis ~1 Mio. Records. Splink mit Spark für 10 Mio.+.
Reicht das DSGVO-konform? Solange du PII-Felder vor Vergleich nicht extern teilst und Logs maskierst, ja. Für Cross-Org-Linkage brauchst du Auftragsverarbeitungs-Vertrag.
Wie oft dedupen? Als Pipeline-Stage, nicht One-Off. Mit jedem neuen Batch neue Duplikate.
Stand: 14. Mai 2026.
Geschrieben von
Harbinger Team
Cloud-, Data- und AI-Engineer in DACH. Schreibt seit 2018 über infrastrukturkritische Tech-Entscheidungen — keine Marketing- Folien, sondern echte Trade-offs aus Production-Workloads.
Hat dir das geholfen?
Jede Woche ein neuer Artikel über DACH-Cloud, Data und AI — direkt in dein Postfach. Kein Spam, kein Marketing-Sprech.
Kein Spam. 1-Klick-Abmeldung. Datenschutz bei Loops.so.