Toute base de données clients grossit avec le temps. Nouvelles saisies, imports CSV, fusions d’entreprises, formulaires en ligne mal filtrés : les doublons s’accumulent silencieusement. Un client peut apparaître sous « Martin Dupont », « M. Dupont », « Dupont Martin », ou avec une adresse e-mail légèrement différente. Pour les équipes commerciales, comptables ou marketing, ces doublons créent des erreurs, des doublons de communication et une perte de confiance dans les données.
Les approches classiques de déduplication (comparaison exacte, distance de Levenshtein) fonctionnent pour les cas simples. Mais elles échouent sur les variations sémantiques, les abréviations, les inversions prénom/nom ou les entreprises avec plusieurs noms commerciaux. C’est là que les embeddings deviennent puissants.
Dans ce tutoriel, vous allez construire un système de détection de doublons basé sur des représentations vectorielles de vos données clients, capable de détecter des similarités que les outils classiques manquent.
Comprendre les embeddings appliqués à la déduplication
Un embedding est une représentation numérique d’un texte sous forme de vecteur dans un espace à plusieurs dimensions. L’idée clé est que deux textes sémantiquement proches auront des vecteurs proches dans cet espace.
Pourquoi les embeddings surpassent les approches classiques
- Distance de Levenshtein : mesure les différences de caractères. « Jean Martin » vs « Martin Jean » = distance élevée malgré l’identité probable
- Comparaison exacte : ne capte pas « SARL Diallo » vs « Diallo Transports SARL »
- Embeddings : capturent la similarité sémantique même avec des formulations différentes
Les modèles d’embeddings utiles pour ce cas
- sentence-transformers (all-MiniLM-L6-v2) : rapide, efficace pour les textes courts en anglais
- paraphrase-multilingual-MiniLM-L12-v2 : multilingue, excellent pour le français
- Ollama + embeddings locaux : pour un déploiement entièrement local et confidentiel
💡 Conseil : pour des données clients françaises ou africaines, privilégiez un modèle multilingue. Les noms propres, les termes d’adresse et les noms d’entreprises varient fortement selon les contextes culturels et linguistiques.
Préparer les données clients pour la déduplication
Structure typique d’une base clients
import pandas as pd
# Exemple de base clients avec potentiels doublons
data = {
"client_id": [1, 2, 3, 4, 5, 6],
"first_name": ["Moussa", "Moussa", "Awa", "Sophie", "Soph.", "Abdou"],
"last_name": ["Ndiaye", "N'Diaye", "Fall", "Dupont", "Dupont", "Diallo"],
"company": ["Diallo Transport SARL", "Ste Diallo Transports", "Fall Distribution", "Dupont Conseil", "Conseil Dupont SAS", "Diallo & Fils"],
"email": ["moussa@diallo.sn", "commercial@diallo.sn", "contact@falldist.sn", "s.dupont@gmail.com", "sophie.dupont@outlook.com", "a.diallo@gmail.com"]
}
df = pd.DataFrame(data)
print(df)
Créer un champ de comparaison unifié
def create_comparison_field(row) -> str:
"""Crée un champ texte unifié pour la comparaison."""
parts = []
# Normaliser prénom + nom
name = f"{row.get('first_name', '')} {row.get('last_name', '')}".strip().lower()
parts.append(name)
# Ajouter l'entreprise si disponible
if row.get('company'):
parts.append(row['company'].lower())
# Ajouter le domaine e-mail comme signal supplémentaire
if row.get('email') and '@' in row['email']:
domain = row['email'].split('@')[1].lower()
parts.append(domain)
return " | ".join(parts)
df['comparison_field'] = df.apply(create_comparison_field, axis=1)
print(df['comparison_field'])
Générer les embeddings et calculer les similarités
Installation et génération avec sentence-transformers
pip install sentence-transformers numpy pandas scikit-learn
from sentence_transformers import SentenceTransformer
import numpy as np
# Charger le modèle multilingue
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# Générer les embeddings pour tous les clients
texts = df['comparison_field'].tolist()
embeddings = model.encode(texts, normalize_embeddings=True)
print(f"Shape des embeddings: {embeddings.shape}")
# (6, 384) - 6 clients, vecteurs de dimension 384
Calculer la matrice de similarité
from sklearn.metrics.pairwise import cosine_similarity
# Matrice de similarité cosinus
similarity_matrix = cosine_similarity(embeddings)
# Convertir en DataFrame pour la lisibilité
sim_df = pd.DataFrame(
similarity_matrix,
index=df['client_id'],
columns=df['client_id']
)
# Afficher les paires avec similarité > 0.8
threshold = 0.80
pairs = []
n = len(df)
for i in range(n):
for j in range(i + 1, n):
score = similarity_matrix[i][j]
if score > threshold:
pairs.append({
"id_1": df.iloc[i]['client_id'],
"nom_1": df.iloc[i]['comparison_field'],
"id_2": df.iloc[j]['client_id'],
"nom_2": df.iloc[j]['comparison_field'],
"similarite": round(score, 4)
})
duplicates_df = pd.DataFrame(pairs).sort_values('similarite', ascending=False)
print(duplicates_df)
Construire un pipeline complet de déduplication
Système de détection automatique
class ClientDeduplicator:
def __init__(self, model_name='paraphrase-multilingual-MiniLM-L12-v2', threshold=0.82):
self.model = SentenceTransformer(model_name)
self.threshold = threshold
def prepare_texts(self, df: pd.DataFrame) -> list[str]:
"""Prépare les textes de comparaison."""
return df.apply(create_comparison_field, axis=1).tolist()
def find_duplicates(self, df: pd.DataFrame) -> pd.DataFrame:
"""Trouve les paires de doublons potentiels."""
texts = self.prepare_texts(df)
embeddings = self.model.encode(texts, normalize_embeddings=True)
sim_matrix = cosine_similarity(embeddings)
duplicates = []
for i in range(len(df)):
for j in range(i + 1, len(df)):
score = sim_matrix[i][j]
if score >= self.threshold:
row_i = df.iloc[i]
row_j = df.iloc[j]
duplicates.append({
"id_1": row_i.get('client_id', i),
"label_1": texts[i],
"id_2": row_j.get('client_id', j),
"label_2": texts[j],
"score": round(score, 4),
"confidence": "haute" if score > 0.92 else "moyenne"
})
return pd.DataFrame(duplicates).sort_values('score', ascending=False)
def generate_report(self, df: pd.DataFrame, output_file="doublons_detectes.csv"):
"""Génère un rapport CSV des doublons."""
duplicates = self.find_duplicates(df)
duplicates.to_csv(output_file, index=False, encoding='utf-8-sig')
print(f"✅ {len(duplicates)} paires de doublons détectées")
print(f"📄 Rapport exporté : {output_file}")
return duplicates
# Utilisation
deduplicator = ClientDeduplicator(threshold=0.82)
rapport = deduplicator.generate_report(df)
Gérer la performance sur de grandes bases
La matrice de similarité complète est en O(n²). Pour 10 000 clients, cela représente 50 millions de comparaisons. Il faut optimiser.
Utiliser FAISS pour la recherche approximative
import faiss
def find_duplicates_faiss(df: pd.DataFrame, model, threshold=0.82, top_k=10):
"""Version scalable avec FAISS pour les grandes bases."""
texts = df.apply(create_comparison_field, axis=1).tolist()
embeddings = model.encode(texts, normalize_embeddings=True).astype('float32')
# Construire l'index FAISS
dimension = embeddings.shape[1]
index = faiss.IndexFlatIP(dimension) # produit scalaire = cosinus si normalisé
index.add(embeddings)
# Rechercher les top-k voisins pour chaque client
scores, indices = index.search(embeddings, top_k + 1) # +1 car inclut soi-même
duplicates = []
for i in range(len(df)):
for rank in range(1, top_k + 1): # Skip index 0 (soi-même)
j = indices[i][rank]
score = scores[i][rank]
if j > i and score >= threshold: # éviter les doublons de paires
duplicates.append({
"id_1": df.iloc[i].get('client_id', i),
"id_2": df.iloc[j].get('client_id', j),
"score": round(float(score), 4)
})
return pd.DataFrame(duplicates)
# Résultats
dupes_faiss = find_duplicates_faiss(df, model)
print(f"Doublons trouvés : {len(dupes_faiss)}")
💡 Conseil : sur des bases de plus de 50 000 contacts, combinez le clustering géographique (regrouper d’abord par ville ou domaine e-mail) avec la recherche FAISS. Cela réduit considérablement le nombre de comparaisons sans perdre en qualité de détection.
Valider et traiter les doublons détectés
Processus de validation humaine
Ne supprimez jamais automatiquement des enregistrements basés uniquement sur un score de similarité. Mettez en place un processus de validation :
- Score > 0.95 : doublon très probable, traitement automatique possible avec notification
- Score entre 0.82 et 0.95 : à valider manuellement par un opérateur
- Score < 0.82 : non signalé, trop de faux positifs
Interface simple de validation
def create_validation_interface(duplicates_df: pd.DataFrame) -> pd.DataFrame:
"""Prépare un fichier de validation pour les opérateurs."""
validation = duplicates_df.copy()
validation['décision'] = '' # 'fusionner', 'ignorer', 'examiner'
validation['traite_par'] = ''
validation['date_traitement'] = ''
# Colonnes pour faciliter la décision
validation['lien_client_1'] = validation['id_1'].apply(
lambda x: f"https://votre-crm.com/clients/{x}"
)
validation['lien_client_2'] = validation['id_2'].apply(
lambda x: f"https://votre-crm.com/clients/{x}"
)
output_file = "doublons_a_valider.xlsx"
validation.to_excel(output_file, index=False)
print(f"Fichier de validation créé : {output_file}")
return validation
Conclusion
Détecter les doublons dans une base clients avec des embeddings est une approche bien plus robuste que les méthodes classiques. Elle capturé les similarités sémantiques, gère les variations orthographiques et culturelles, et s’adapté naturellement au français et aux langues africaines.
En combinant un modèle multilingue comme sentence-transformers, un index FAISS pour la performance, et un processus de validation humaine pour les cas ambigus, vous obtenez un système de déduplication fiable, scalable et adaptable à vos données.
La qualité d’une base clients déterminé directement la qualité de vos actions marketing, commerciales et comptables. Un investissement dans la déduplication rapporte rapidement en termes d’efficacité opérationnelle.
Vous voulez aller plus loin sur l’IA appliquée aux données métier ? Retrouvez nos tutoriels sur ITSkillsCenter.io pour développer des compétences concrètes en Python, NLP et automatisation.
Étape 1 : poser le problème métier sur une base clients ouest-africaine
Avant d’écrire la moindre ligne de code, il faut clarifier ce qu’est un « doublon » dans votre contexte. Sur une base CRM à Dakar, Abidjan ou Lomé, deux fiches peuvent désigner la même personne malgré des graphies différentes : « Mamadou Diallo » et « M. Diallo », « Aïcha Ndiaye » et « Aicha N’Diaye », ou encore « Boutique Chez Fatou » et « Boutique chez Fatou SARL ». Les approches SQL classiques (égalité stricte ou LIKE) échouent dès qu’un accent, une abréviation ou un suffixe juridique apparaît.
Listez d’abord les champs sur lesquels la déduplication doit opérer : nom complet, téléphone (format +221, +225, +228), email, adresse. Décidez si vous tolérez les faux positifs (préférable pour un onboarding marketing) ou les faux négatifs (préférable pour la facturation). Cette décision orientera le seuil de similarité que vous fixerez à l’étape 6.
Étape 2 : préparer l’environnement Python avec pgvector
Nous utiliserons Postgres 17 avec l’extension pgvector 0.8 pour stocker les embeddings et exécuter des requêtes de similarité cosinus directement en base. Sur Ubuntu 24.04, installez les paquets puis activez l’extension dans votre base.
sudo apt install postgresql-17 postgresql-17-pgvector
sudo -u postgres psql -d clients_db -c "CREATE EXTENSION vector;"
python3.12 -m venv .venv && source .venv/bin/activate
pip install openai psycopg[binary] python-dotenv pandas
La sortie attendue de la dernière commande affiche « Successfully installed openai-1.x psycopg-3.x ». Si pgvector ne s’installe pas via apt (cas fréquent sur les VPS Contabo Francfort utilisés depuis le Sénégal), compilez-le depuis les sources GitHub officielles, l’opération prend moins de deux minutes.
Étape 3 : nettoyer les données avant vectorisation
Un embedding capture le sens, mais la qualité dépend du prétraitement. Normalisez la casse, retirez les caractères de contrôle, harmonisez les indicatifs téléphoniques et supprimez les suffixes juridiques redondants (« SARL », « SUARL », « GIE ») pour qu’ils ne pèsent pas trop dans la représentation vectorielle.
import re, unicodedata
def normaliser(fiche):
nom = unicodedata.normalize("NFKC", fiche["nom"]).strip().lower()
nom = re.sub(r"\s+(sarl|suarl|gie|sa)\b", "", nom)
tel = re.sub(r"[^0-9+]", "", fiche.get("tel", ""))
return f"{nom} | {tel} | {fiche.get('email','').lower()}"
Cette fonction renvoie une chaîne canonique : « fatou ndiaye | +221771234567 | fatou@example.sn ». C’est cette chaîne — et non la fiche brute — qui sera passée au modèle d’embedding. Vérifiez sur dix fiches au hasard que la sortie est cohérente avant de lancer le batch complet.
Étape 4 : générer les embeddings avec text-embedding-3-small
Le modèle text-embedding-3-small d’OpenAI produit des vecteurs de 1536 dimensions pour environ 0,02 USD pour un million de tokens, soit moins de 13 FCFA pour 1 000 fiches clients moyennes. Pour rester souverain, vous pouvez aussi exécuter localement nomic-embed-text via Ollama sur un VPS à 8 Go de RAM.
from openai import OpenAI
client = OpenAI()
def embed(texte):
r = client.embeddings.create(model="text-embedding-3-small", input=texte)
return r.data[0].embedding
Bouclez sur vos fiches par lots de 100 pour limiter les appels réseau et stockez chaque vecteur en base via INSERT INTO clients_emb (client_id, emb) VALUES (%s, %s). Surveillez la latence : depuis Dakar vers les serveurs OpenAI, comptez 200 à 400 ms par batch.
Étape 5 : indexer pour la recherche de voisins proches
Sans index, une requête de similarité scanne toute la table. À partir de 5 000 fiches, créez un index HNSW qui maintient un graphe de proximité et répond en quelques millisecondes.
CREATE INDEX ON clients_emb USING hnsw (emb vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
L’index se construit en arrière-plan, vous pouvez continuer à insérer pendant la création. Vérifiez ensuite avec EXPLAIN ANALYZE qu’une requête de recherche utilise bien Index Scan using clients_emb_emb_idx et non Seq Scan.
Étape 6 : détecter les doublons avec un seuil de similarité
Deux vecteurs identiques ont une distance cosinus de 0, deux vecteurs orthogonaux de 1. Empiriquement sur des bases clients francophones, un seuil de 0,15 capture la plupart des doublons sans générer trop de faux positifs. Ajustez ce seuil après une première passe manuelle sur 50 paires.
SELECT a.client_id, b.client_id, a.emb <=> b.emb AS distance
FROM clients_emb a JOIN clients_emb b
ON a.client_id < b.client_id
WHERE a.emb <=> b.emb < 0.15
ORDER BY distance ASC;
Cette requête retourne les paires candidates classées de la plus proche à la moins proche. Sur une base de 10 000 fiches, attendez-vous à quelques centaines de paires à arbitrer manuellement la première fois.
Étape 7 : valider les paires candidates avec un humain dans la boucle
L’embedding propose, l’humain dispose. Construisez une petite interface (Streamlit ou Notion partagé) listant les paires avec les champs sources côte à côte, et trois boutons : « Fusionner », « Garder séparées », « Reporter ». Stockez les décisions dans une table decisions_doublons pour entraîner plus tard un seuil personnalisé.
Cette boucle de validation, indispensable les premières semaines, permet de découvrir des cas limites propres à votre activité — par exemple, en téléphonie mobile au Sénégal, deux numéros différents sur Mixx by Yas et Orange peuvent appartenir au même client professionnel.
Étape 8 : industrialiser le pipeline en cron quotidien
Programmez un job nocturne qui embed les nouvelles fiches insérées dans la journée, lance la requête de détection sur le delta, et notifie l’équipe commerciale par email ou WhatsApp Cloud API des paires à arbitrer. Pour creuser ce sujet, lisez nos guides Postgres pgvector pour la recherche sémantique et Industrialiser un ETL Postgres en cron.
0 2 * * * cd /opt/dedup && .venv/bin/python pipeline.py >> logs/cron.log 2>&1
Vérifiez chaque matin la taille du fichier de log : une exécution saine produit environ 200 octets par ligne et se termine sur « Pipeline OK · N paires détectées ».
Etape 1 : comprendre pourquoi un MERGE SQL ne suffit plus
Une base clients reelle accumule des doublons subtils : Aminata Diop et Aminata DIOP, Fatou Sarr ecrite Fatu Sar, ou un meme client saisi avec deux numeros differents (Orange et Mixx by Yas). Une jointure SQL classique sur (nom, prenom) ou un GROUP BY ne capte pas ces variantes. Resultat : la PME possede 3 fiches pour le meme client, fausse les segments marketing et envoie 3 SMS a la meme personne.
Les embeddings transforment chaque fiche client en un vecteur de 384 a 1 536 dimensions, qui capture le sens et les variations orthographiques. Deux fiches presque identiques produisent deux vecteurs proches dans cet espace, mesurables par similarite cosinus. C’est cette propriete qu’on exploite pour detecter les doublons sans regle metier rigide.
Etape 2 : preparer un environnement Python minimal
L’approche la plus fiable et la moins couteuse en 2026 reste l’open source : le modele BGE-M3 (BAAI) ou sentence-transformers/all-MiniLM-L6-v2 tournent en local, sans envoi de donnees clients vers un fournisseur tiers (point cle pour la conformite loi 2008-12). Installez sur un poste Python 3.12 :
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# .venv\Scripts\activate sur Windows
pip install sentence-transformers pandas scikit-learn numpy
L’execution se fait en CPU pour quelques milliers de fiches ; au-dela de 50 000 lignes, un GPU ou un service cloud (Replicate, Hugging Face Inference API) accelere considerablement. Verifiez la version : python -c "import sentence_transformers; print(sentence_transformers.__version__)" doit afficher au minimum 3.0.
Etape 3 : normaliser les fiches avant vectorisation
Les embeddings tolerent les variations mais une normalisation legere ameliore le rappel. Concatenez les champs en une seule chaine, en minuscules, sans accents superflus. L’objectif est de produire pour chaque fiche un texte canonique qui represente l’identite du client.
import pandas as pd, unicodedata
df = pd.read_csv('clients.csv')
def canon(row):
parts = [str(row[c]) for c in ['prenom','nom','telephone','email','ville']]
s = ' '.join(parts).lower()
s = unicodedata.normalize('NFKD', s).encode('ascii','ignore').decode()
return ' '.join(s.split())
df['canon'] = df.apply(canon, axis=1)
print(df[['id_client','canon']].head())
Verifiez quelques sorties manuellement : la chaine canon doit rester lisible. Une normalisation trop agressive (suppression de ponctuation, stemming) detruit l’information utile et cree de faux positifs. Conservez les chiffres des numeros de telephone, ils discriminent bien deux clients homonymes.
Etape 4 : generer les embeddings en batch
Le modele charge une fois en memoire encode des milliers de phrases en quelques minutes. Encodez en batch pour exploiter la vectorisation interne du framework : un appel par ligne est dix fois plus lent qu’un appel sur une liste.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
embeddings = model.encode(
df['canon'].tolist(),
batch_size=64,
show_progress_bar=True,
normalize_embeddings=True # vecteurs unitaires, cosinus = produit scalaire
)
np.save('embeddings.npy', embeddings)
print(embeddings.shape) # (n_lignes, 384)
Le parametre normalize_embeddings=True simplifie ensuite le calcul : la similarite cosinus devient un simple produit scalaire. La sortie pour 10 000 fiches en CPU prend environ 90 secondes sur un laptop recent. Sauvegarder les vecteurs evite de recalculer a chaque execution du script.
Etape 5 : detecter les paires similaires avec un seuil
Deux strategies existent. La methode brute calcule la matrice complete de similarite, pratique jusqu’a 20 000 fiches mais quadratique en memoire. Au-dela, on utilise un index approximatif (FAISS, ScaNN) qui retourne pour chaque vecteur ses k plus proches voisins en O(log n).
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity(embeddings)
np.fill_diagonal(sim, 0) # ignorer l'auto-correspondance
# seuil empirique : 0.92 pour clients particuliers
threshold = 0.92
pairs = np.argwhere(sim > threshold)
pairs = pairs[pairs[:,0] < pairs[:,1]] # eviter les doublons (i,j)/(j,i)
print(f"{len(pairs)} paires de doublons potentiels detectees")
Calibrez le seuil sur un echantillon : 0.92 attrape les variantes orthographiques sans trop de faux positifs ; 0.85 est plus laxiste, utile pour les bases tres bruitees mais demande une revue humaine. Ne fusionnez jamais automatiquement au-dessus de 0.95 sans validation : un homonyme en zone rurale (deux Aminata Diop a Thies) reste possible.
Etape 6 : valider et fusionner les doublons
Exportez les paires en CSV avec les champs originaux des deux fiches cote a cote, plus la similarite. Faites valider par une personne du metier (responsable CRM, chef d’agence) avant toute fusion. La regle de fusion type : conserver la fiche avec la date de creation la plus ancienne (c’est l’ID historique), et reporter sur elle les attributs les plus recents (telephone actuel, adresse a jour).
Pour la fusion en base SQL, encadrez l’operation par une transaction et conservez une table d’audit doublons_fusion qui trace : id_conserve, id_supprime, date_fusion, operateur. Vous pourrez ainsi annuler une fusion erronee a posteriori. C’est cette couche d’audit qui rend l’approche acceptable pour la direction.
Etape 7 : industrialiser en pipeline mensuel
Un nettoyage one-shot reduit la base d’un coup, mais les doublons reviennent. Planifiez un pipeline mensuel : extraction CSV depuis l’ERP (Odoo, Sage, Sellsy), execution du script Python, export des paires suspectes vers un dashboard Metabase ou un tableur partage, validation hebdomadaire par le responsable CRM, fusion en base. Documentez chaque etape dans un README pour la passation.
Pour les bases tres volumineuses (au-dela de 500 000 fiches), envisagez une base vectorielle dediee : Qdrant ou Weaviate self-hosted sur un VPS Hostinger Dakar coutent 25 EUR (16 400 FCFA) par mois et stockent les embeddings durablement. Le pipeline ne calcule alors que les vecteurs des nouvelles fiches, pas la matrice complete a chaque run. Pour creuser ce sujet, voyez aussi notre guide data quality en PME et notre tutoriel Python pour l’analyse de donnees.