Limpiar duplicados de CRM con Python 2026 sigue un patrón claro: normalizar phone a formato E.164 con la librería phonenumbers, normalizar email (lowercase, trim, sin dot tricks de Gmail), aplicar fuzzy matching con RapidFuzz o fuzzywuzzy sobre nombre con threshold 85 por ciento, y usar recordlinkage o dedupe para casos probabilísticos complejos. El stack típico es pandas más phonenumbers más RapidFuzz, con script reproducible que produce un CSV de duplicados sugeridos para revisión humana antes de hacer merge en el CRM. Para evitar que vuelvan, hay que pegar un proceso continuo en la entrada de cada nuevo contact. Pyme LATAM típica encuentra entre 10 y 30 por ciento de duplicados en su CRM.
Si manejas HubSpot, Pipedrive, Zoho, Salesforce, Mercado Libre o cualquier base de contacts y sospechas que tienes duplicados, esta guía te da el patrón técnico ganador.
Paso 1: Inventario y métricas iniciales
Antes de tocar datos:
- Exportar contacts completos a CSV o Parquet
- Conteo total de registros
- Conteo de registros con phone vacío y email vacío
- Distribución de phone format (cuántos sin código país, cuántos con guiones, etc.)
- Distribución de email format (cuántos con typos comunes, gmial.com, etc.)
Sin estos números base, no puedes medir si el dedup funcionó.
Paso 2: Normalizar phone a E.164
E.164 es el formato internacional estándar: plus52 55 1234 5678 se vuelve plus5215512345678. La librería phonenumbers de Python (port del Google libphonenumber):
import phonenumbers
def normalize_phone(raw, default_country='MX'):
try:
parsed = phonenumbers.parse(raw, default_country)
if not phonenumbers.is_valid_number(parsed):
return None
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
except Exception:
return None
df['phone_e164'] = df['phone_raw'].apply(normalize_phone)
Importante: pasar default_country correcto por origen del lead (MX, AR, CO, CL, PE, GT). Sin eso, phonenumbers no sabe interpretar números locales sin código país.
Paso 3: Normalizar email
Email es sensible:
def normalize_email(raw):
if not raw or '@' not in raw:
return None
email = raw.lower().strip()
local, domain = email.split('@', 1)
if domain == 'gmail.com':
local = local.replace('.', '').split('+')[0]
return f"{local}@{domain}"
df['email_norm'] = df['email_raw'].apply(normalize_email)
Para Gmail, eliminar dots y plus tricks porque Gmail los ignora pero usuarios crean cuentas distintas con esos trucos sin querer.
Paso 4: Detectar duplicados duros
Duplicados duros son los que coinciden en phone o email:
import pandas as pd
# Duplicados por phone normalizado
dup_by_phone = df[df.duplicated(subset=['phone_e164'], keep=False)]
dup_by_phone = dup_by_phone.sort_values(['phone_e164', 'created_at'])
# Duplicados por email normalizado
dup_by_email = df[df.duplicated(subset=['email_norm'], keep=False)]
# Marcar
df['is_dup_phone'] = df['phone_e164'].duplicated(keep='first')
df['is_dup_email'] = df['email_norm'].duplicated(keep='first')
Pyme LATAM típica encuentra 5 a 15 por ciento de duplicados duros en este paso.
Paso 5: Fuzzy matching sobre nombre
Para contacts sin phone o email coincidente pero con nombre parecido:
from rapidfuzz import process, fuzz
def find_similar_names(name, candidates, threshold=85):
matches = process.extract(name, candidates, scorer=fuzz.ratio, limit=5)
return [m for m in matches if m[1] >= threshold and m[0] != name]
Threshold 85 captura "Juan Perez", "Juan Pérez", "Juan F Perez" como similares. Threshold 95 es más estricto. Threshold 75 da muchos falsos positivos.
Paso 6: recordlinkage para casos probabilísticos
Cuando dos contacts tienen nombre similar, ciudad similar, empresa similar pero phone y email distintos, recordlinkage da un score probabilístico:
import recordlinkage
from recordlinkage.preprocessing import clean
indexer = recordlinkage.Index()
indexer.block('zip_code')
candidate_pairs = indexer.index(df)
compare = recordlinkage.Compare()
compare.string('name_clean', 'name_clean', method='jarowinkler', threshold=0.85, label='name')
compare.string('company_clean', 'company_clean', method='jarowinkler', threshold=0.85, label='company')
compare.exact('city', 'city', label='city')
features = compare.compute(candidate_pairs, df)
matches = features[features.sum(axis=1) >= 2.5]
Esto devuelve pares candidatos con score acumulado. Score mayor a 2.5 sobre 3 indica alta probabilidad de duplicado.
Paso 7: Merge guiado por humano
NO mergear automático. Después de tener candidatos:
- Exportar pares a CSV con columnas: id_A, id_B, score, motivo
- Equipo de ventas o de operaciones revisa lote de 50 a 100 a la vez
- Decide quién es master y quién se mergea
- Script aplica merge llamando a API del CRM con el master_id
HubSpot, Pipedrive y Salesforce todos tienen endpoint merge contacts en su API. Zoho también.
Paso 8: Prevenir duplicados futuros
Sin proceso continuo, duplicados vuelven en 3 a 6 meses:
- Webhook en formulario llama a tu API con phone más email crudos
- API normaliza con misma lógica del script de limpieza
- API busca match en CRM por phone normalizado o email normalizado
- Si match: actualiza contact existente con datos nuevos
- Si no match: crea contact nuevo
Esto es lo que evita que el CRM se vuelva a contaminar.
El caso real: HubSpot rate limit y dedup phone variantes
Una escuela educativa en Huixquilucan tenía HubSpot saturado con duplicados por phone variants. Catalizadora implementó:
- Phone normalization en pre check antes de crear contact en HubSpot
- 5 variantes phone consolidadas en una sola call API con filterGroups OR
- Búsqueda por nombre cross match para meeting booking detection
- 80 por ciento reducción API calls por ciclo (de 100 más a 20)
- Zero 429 errors después del deploy
- Cache 15 minutos para variantes ya buscadas
- Inversión: 1 día, 0 USD directo, incluido en honorarios mensuales
Resultado: dashboard CEO con atribución multi canal demostrable y 7 de 7 inscritos trazables.
Lo que NO debes hacer
- Mergear automático sin revisión humana: pierdes datos importantes (notas de cliente A que no estaban en cliente B)
- Sin normalizar phone antes de comparar: dedup falla en 30 a 50 por ciento de casos LATAM
- Sin Git para tu script: cuando algo falla en producción, no sabes qué cambió
- Sin proceso continuo: limpias hoy y duplicados vuelven en 6 meses
Próximos pasos
Si tu CRM LATAM (HubSpot, Pipedrive, Salesforce, Zoho, Mercado Libre, propio) tiene 10 a 30 por ciento de duplicados, un script Python con phonenumbers más RapidFuzz más recordlinkage los detecta en 1 a 4 semanas según volumen.
Catalizadora arma el diagnóstico en una llamada de 30 minutos, sin pitch deck, conversación real sobre tu operación.
- MAGIA Core construye sistemas a medida con dedup, integraciones profundas y dashboards en 12 semanas por 15,000 USD. Código a tu nombre, sin retainers ni licencias atadas.
- Para pymes pequeñas con CRM propio listo en 15 días, MAGIA Solo cubre desde 4,500 USD.