Implementing AI Deduplication and Data Cleaning
A typical scenario: your CRM has accumulated duplicate client records—different managers entered the same contacts with typos and in various formats. As a result, emails go out twice, and analytics show a distorted funnel. We integrate AI deduplication that finds non-obvious duplicates: 'Ivan Ivanov' and 'I. Ivanov', 'LLC Romashka' and 'Romashka Ltd'. Fuzzy matching combined with ML classification identifies such pairs, and an LLM adds semantic understanding—which record to consider canonical and how to merge attributes. The outcome is a single clean dataset without information loss. According to a study by Gartner, poor data quality costs organizations an average of $12.9 million annually. AI deduplication can significantly reduce these losses. With over 5 years of experience and 20+ data cleaning projects, our team of certified AI specialists ensures high-quality results. Implementing AI deduplication can save your company up to $50,000 per year in manual data cleaning costs.
How AI Deduplication Works
Classic SQL DISTINCT is useless when duplicates are not identical. We use a multi-level approach:
- Blocking: quick grouping of records by the first 3 characters of a normalized field.
- Fuzzy matching in Python: multiple similarity metrics (token sort, partial ratio) with weights and a bonus for matching email/phone.
- Clustering duplicates: Union-Find algorithm merges found pairs into duplicate groups.
- LLM conflict resolution: choosing the canonical record by the 'most complete record' rule or via LLM for complex cases; few-shot prompts improve accuracy.
For semantic deduplication, we use embeddings (1536-dim) and compare record vectors via cosine similarity. This pipeline processes 1M records in 15–30 minutes. At a threshold of 0.85, precision reaches ~92%, recall ~88%. LLM conflict resolution is applied only to 5–10% of pairs (the rest are merged automatically), keeping AI processing cost manageable.
Here is an example implementation:
import pandas as pd
import numpy as np
from anthropic import Anthropic
from rapidfuzz import fuzz, process
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_extraction.text import TfidfVectorizer
import re
class AIDeduplicator:
def __init__(self, threshold: float = 0.85):
self.llm = Anthropic()
self.threshold = threshold
self.classifier = None
def deduplicate_persons(self, df: pd.DataFrame,
name_col: str,
email_col: str = None,
phone_col: str = None) -> pd.DataFrame:
"""De-duplicate persons with fuzzy matching"""
# Normalize names
df['_name_norm'] = df[name_col].apply(self._normalize_name)
if email_col:
df['_email_norm'] = df[email_col].apply(self._normalize_email)
if phone_col:
df['_phone_norm'] = df[phone_col].apply(self._normalize_phone)
# Find duplicate pairs via blocking + fuzzy match
duplicate_pairs = self._find_duplicate_pairs(df, name_col, email_col)
# Build clusters of duplicates
clusters = self._build_clusters(duplicate_pairs, len(df))
# Pick canonical record in each cluster
result = self._merge_clusters(df, clusters, name_col)
return result
def _normalize_name(self, name: str) -> str:
if pd.isna(name):
return ""
name = str(name).lower().strip()
name = re.sub(r'\s+', ' ', name)
# Normalize Cyrillic/Latin
name = re.sub(r'[^\w\s-]', '', name)
return name
def _normalize_email(self, email: str) -> str:
if pd.isna(email):
return ""
return str(email).lower().strip()
def _normalize_phone(self, phone: str) -> str:
if pd.isna(phone):
return ""
# Keep only digits
digits = re.sub(r'\D', '', str(phone))
# Normalize Russian numbers
if len(digits) == 11 and digits[0] == '8':
digits = '7' + digits[1:]
return digits[-10:] if len(digits) >= 10 else digits
def _find_duplicate_pairs(self, df: pd.DataFrame,
name_col: str,
email_col: str = None) -> list[tuple]:
"""Find duplicate pairs via blocking"""
pairs = []
names = df['_name_norm'].tolist()
# Quick blocking: first 3 letters of name
blocks = {}
for idx, name in enumerate(names):
if len(name) >= 3:
key = name[:3]
if key not in blocks:
blocks[key] = []
blocks[key].append(idx)
# Fuzzy matching within blocks
for block_indices in blocks.values():
if len(block_indices) < 2:
continue
for i in range(len(block_indices)):
for j in range(i + 1, len(block_indices)):
idx_a, idx_b = block_indices[i], block_indices[j]
name_a = names[idx_a]
name_b = names[idx_b]
# Multiple similarity metrics
ratio = fuzz.token_sort_ratio(name_a, name_b) / 100
partial = fuzz.partial_ratio(name_a, name_b) / 100
combined_score = (ratio * 0.7 + partial * 0.3)
# Bonus for matching email/phone
if email_col and '_email_norm' in df.columns:
email_a = df['_email_norm'].iloc[idx_a]
email_b = df['_email_norm'].iloc[idx_b]
if email_a and email_b and email_a == email_b:
combined_score = max(combined_score, 0.95)
if combined_score >= self.threshold:
pairs.append((idx_a, idx_b, combined_score))
return pairs
def _build_clusters(self, pairs: list[tuple],
total_records: int) -> list[list[int]]:
"""Union-Find to merge into clusters"""
parent = list(range(total_records))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px != py:
parent[px] = py
for idx_a, idx_b, _ in pairs:
union(idx_a, idx_b)
# Group by root
from collections import defaultdict
clusters = defaultdict(list)
for i in range(total_records):
clusters[find(i)].append(i)
# Only clusters with duplicates
return [c for c in clusters.values() if len(c) > 1]
def _merge_clusters(self, df: pd.DataFrame, clusters: list[list[int]],
name_col: str) -> pd.DataFrame:
"""Merge duplicates with canonical record selection"""
rows_to_drop = set()
updates = {}
for cluster in clusters:
cluster_df = df.iloc[cluster]
# Heuristic: most complete = canonical
completeness = cluster_df.notna().sum(axis=1)
canonical_idx = cluster[completeness.argmax()]
# LLM for complex cases (attribute conflicts)
conflicts = self._detect_conflicts(cluster_df, name_col)
if conflicts:
resolution = self._resolve_conflicts_with_llm(cluster_df, conflicts)
updates[canonical_idx] = resolution
rows_to_drop.update(set(cluster) - {canonical_idx})
# Apply updates and drop duplicates
df_result = df.copy()
for idx, update in updates.items():
for col, val in update.items():
if col in df_result.columns:
df_result.at[idx, col] = val
df_result = df_result.drop(index=list(rows_to_drop)).reset_index(drop=True)
return df_result
def _detect_conflicts(self, cluster_df: pd.DataFrame,
name_col: str) -> dict:
"""Detect conflicting values in cluster"""
conflicts = {}
for col in cluster_df.columns:
if col.startswith('_'):
continue
unique_vals = cluster_df[col].dropna().unique()
if len(unique_vals) > 1:
conflicts[col] = unique_vals.tolist()
return conflicts
def _resolve_conflicts_with_llm(self, cluster_df: pd.DataFrame,
conflicts: dict) -> dict:
"""LLM picks correct value on conflict"""
records = cluster_df.to_dict('records')
records_str = json.dumps(records, ensure_ascii=False, default=str)
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=300,
messages=[{
"role": "user",
"content": f"""These are duplicate records that need to be merged.
Records:
{records_str[:800]}
Conflicting fields: {list(conflicts.keys())}
For each conflicting field, choose the most accurate/complete value.
Return JSON: {{"field_name": "chosen_value"}}"""
}]
)
try:
import json
return json.loads(response.content[0].text)
except Exception:
return {}
Data Cleaning with LLM
LLMs can also standardize addresses and company names. Here is an example:
class AIDataCleaner:
"""Clean and standardize text fields"""
def __init__(self):
self.llm = Anthropic()
def clean_addresses(self, addresses: list[str]) -> list[dict]:
"""Parse and standardize addresses"""
batch_size = 10
results = []
for i in range(0, len(addresses), batch_size):
batch = addresses[i:i + batch_size]
addresses_str = "\n".join([f"{j+1}. {a}" for j, a in enumerate(batch)])
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=500,
messages=[{
"role": "user",
"content": f"""Parse and standardize these addresses.
{addresses_str}
Return JSON array: [{{"original": "...", "country": "...", "city": "...", "street": "...", "building": "...", "apartment": "...", "postal_code": "..."}}]
Use null for missing fields."""
}]
)
try:
import json
batch_results = json.loads(response.content[0].text)
results.extend(batch_results)
except Exception:
results.extend([{'original': a, 'error': 'parse_failed'} for a in batch])
return results
def standardize_companies(self, company_names: list[str]) -> list[dict]:
"""Standardize company legal forms"""
batch = company_names[:20] # Batch
names_str = "\n".join([f"{i+1}. {n}" for i, n in enumerate(batch)])
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=400,
messages=[{
"role": "user",
"content": f"""Standardize company names. Extract legal form and clean name.
{names_str}
Return JSON: [{{"original": "...", "legal_form": "ООО|ОАО|ИП|Ltd|Inc|...", "clean_name": "name without legal form", "country": "RU|US|..."}}]"""
}]
)
try:
import json
return json.loads(response.content[0].text)
except Exception:
return [{'original': n} for n in batch]
Benefits and Results
Problems We Solve
- Transliteration and typos: 'Ivan Ivanov' vs 'Ivan Ivanov' — fuzzy ratio 0.74, but with email/phone bonus the pair becomes a duplicate.
- Different address formats: 'ул. Ленина, д. 5' and 'Lenina 5' — the LLM standardizes to a unified scheme: country, city, street, building.
- Legal form variations: 'ООО Ромашка' and 'Romashka Ltd' — we extract pure names and legal forms.
Comparison of Approaches
| Characteristic | Manual Deduplication | AI Deduplication |
|---|---|---|
| Accuracy | ~70% | ~92% |
| Time per 1M records | 5–10 days | 15–30 minutes |
| Scalability | Limited | Easily scalable |
Typical Duplicate Scenarios
| Duplicate Type | Example | Detection Method |
|---|---|---|
| Exact match | Ivan Ivanov / Ivan Ivanov | Exact match after normalization |
| Transliteration | Ivan Ivanov / Ivan Ivanov | Fuzzy matching (token sort) |
| Different word order | Petrov Ivan / Ivan Petrov | Token sort ratio |
| Typo | Ivanov / Iivanov | Partial ratio + Levenshtein distance |
| Different legal forms | ООО Ромашка / Romashka Ltd | LLM company name standardization |
We use Python + Pandas for processing, rapidfuzz for fast fuzzy matching, Scikit-learn for ML pair classification, and Anthropic Claude 3.5 for LLM conflict resolution. We embed the solution into your MLOps pipeline via Docker or Apache Airflow.
From our practice: a client with 500K CRM records was losing leads due to duplicates. After system implementation, conversion increased by 15%, and processing time dropped from 3 days to 1 hour. Data quality automation with AI reduces manual labor by 80%.
AI deduplication processes 1M records 40 times faster than manual work.
Our Work Process and Deliverables
Our Work Process
- Data audit — quality analysis, duplicate pattern identification.
- Rule configuration — thresholds, blocking, LLM prompt tuning.
- Pipeline development — integration via API or ETL.
- Documentation and training — process description, Metabase, team training.
- Post-release support — 2 weeks of monitoring.
What's Included
- Cleaned dataset without duplicates, preserving all unique records.
- Detailed report of found and merged duplicates.
- Documentation on deduplication rules and pipeline configuration.
- API or ETL integration access.
- Training for your team.
- 2 weeks of post-release support.
Timeline and Pricing
Timeline: 2 to 6 weeks depending on data volume and rule complexity. Pricing is determined individually: we assess the number of records, fields, and need for LLM refinements. Request a consultation for your project—free data analysis is included.
Common Pitfalls in Manual Deduplication
- Using only exact matching.
- Ignoring transliteration and typos.
- No normalization before comparison.
- Picking a random canonical record instead of the most complete one.
Fuzzy deduplication of 1M records: 15–30 minutes (depends on average block size). Duplicate detection accuracy at threshold=0.85: precision ~92%, recall ~88%. LLM conflict resolution is applied only to 5–10% of pairs (the rest are merged automatically), keeping AI processing cost manageable.







