Собесов

Сценарий: fuzzy-match для объединения справочников компаний

PythonОчистка данныхСредняяMiddle

Условие

В CRM компания пишется ООО "Ромашка", в платёжной системе — Romashka LLC. Нужно поженить две таблицы по названию. Точный merge не сработает. Как сделать fuzzy-match на 50k × 50k?

Решение

Подход

  1. Нормализация строк (lowercase, убрать формы собственности, пунктуацию, транслитерация).
  2. Блокировка (blocking) — не сравнивать всё со всем, а группировать по первой букве/первым 3 символам/городу.
  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 (устойчив к перестановкам слов)
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]

Подводные камни

  1. Без блокировки 50k × 50k = 2.5 млрд пар — невозможно.
  2. token_sort_ratio устойчив к перестановкам, partial_ratio — к подстрокам. Выбор зависит от природы данных.
  3. Транслитерация: «Ромашка»/«Romashka» — нужно одинаково транслитерировать обе стороны (transliterate/unidecode).
  4. Threshold 85 случайно объединит «Альфа банк» и «Альфа банк страхование». Всегда ручная проверка топ-1000.
  5. Один-ко-многим — fuzzy матчинг даёт N кандидатов; нужна стратегия выбора лучшего (по доп. фичам — ИНН, адрес).

Эталонный ответ

Нормализация → блокировка → rapidfuzz.process.extractOne(scorer=token_sort_ratio) с порогом 85–90 и обязательной ручной проверкой матчей.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти