Условие
В CRM компания пишется ООО "Ромашка", в платёжной системе — Romashka LLC. Нужно поженить две таблицы по названию. Точный merge не сработает. Как сделать fuzzy-match на 50k × 50k?
Решение
Подход
- Нормализация строк (lowercase, убрать формы собственности, пунктуацию, транслитерация).
- Блокировка (blocking) — не сравнивать всё со всем, а группировать по первой букве/первым 3 символам/городу.
- Сравнение в блоке через
rapidfuzz(быстрееfuzzywuzzyв 10×).
Реализация
import re
import pandas as pd
from rapidfuzz import process, fuzz
STOP = {'ооо', 'оао', 'зао', 'ip', 'llc', 'inc', 'ltd', 'gmbh'}
def normalize(s: str) -> str:
s = s.lower()
s = re.sub(r'["«»\'\.,()]', ' ', s)
s = re.sub(r'\s+', ' ', s).strip()
return ' '.join(w for w in s.split() if w not in STOP)
crm['name_norm'] = crm['name'].apply(normalize)
pay['name_norm'] = pay['name'].apply(normalize)
# Блок по первой букве
def match_block(group_left, group_right, threshold=85):
matches = []
right_names = group_right['name_norm'].tolist()
for _, row in group_left.iterrows():
m = process.extractOne(row['name_norm'], right_names, scorer=fuzz.token_sort_ratio)
if m and m[1] >= threshold:
matches.append({'left_id': row['id'], 'right_name': m[0], 'score': m[1]})
return matches
# Можно усилить: token_set_ratio (устойчив к перестановкам слов)Production-вариант — recordlinkage или Splink
import recordlinkage as rl
indexer = rl.Index()
indexer.block('first_letter') # блок-ключ
candidates = indexer.index(crm, pay)
cmp = rl.Compare()
cmp.string('name_norm', 'name_norm', method='jarowinkler', threshold=0.85, label='name_sim')
features = cmp.compute(candidates, crm, pay)
matches = features[features.sum(axis=1) >= 1]Подводные камни
- Без блокировки 50k × 50k = 2.5 млрд пар — невозможно.
token_sort_ratioустойчив к перестановкам,partial_ratio— к подстрокам. Выбор зависит от природы данных.- Транслитерация: «Ромашка»/«Romashka» — нужно одинаково транслитерировать обе стороны (
transliterate/unidecode). - Threshold 85 случайно объединит «Альфа банк» и «Альфа банк страхование». Всегда ручная проверка топ-1000.
- Один-ко-многим — fuzzy матчинг даёт N кандидатов; нужна стратегия выбора лучшего (по доп. фичам — ИНН, адрес).
Эталонный ответ
Нормализация → блокировка → rapidfuzz.process.extractOne(scorer=token_sort_ratio) с порогом 85–90 и обязательной ручной проверкой матчей.