名寄せ(Entity Resolution / Record Linkage)── 同じエンティティを別々の表記で記録した複数レコードを、1つにまとめる作業のこと。「合同会社ふくふく」と「(同)ふくふく」と「ふくふく合同会社」と「Fukufuku LLC」が全部同じ会社だと機械的に判定する。これが分析データの品質を決定づける、地味で膨大な仕事です。
名寄せが甘いと、同じ顧客が 5 回登録されて、売上 100 万 / 平均購入額 20 万 のはずが、売上 100 万 / 平均購入額 4 万 に化けます。すべての顧客分析・ 計算・キャンペーン効果測定の 土台 が狂う。
名寄せの 4 段階アプローチ
| 段階 | 手法 | 計算量 | 精度 | 向く用途 |
|---|---|---|---|---|
| 1. 完全一致 | EQUAL JOIN | O(N) | 100% / カバレッジ低 | ID / メアドが揃ってる場合 |
| 2. 正規化マッチ | 正規化後の完全一致 | O(N) | 高 | 会社名・住所 |
| 3. ファジーマッチ | Levenshtein / Jaro-Winkler | O(N²) | 中〜高 | 100万行までの中規模 |
| 4. マッチ | 学習ベース | 前処理重 | 最高 | 100万行超 / 精度要求高 |
1. 完全一致(最初の網)
メールアドレスや電話番号、社内 ID が共通であれば最も信頼できる。が、実データはほぼ揃わないため、これだけで終わるケースは稀。
-- 同一メアドで複数レコードを統合SELECT email, COUNT(*) AS dup_count, ARRAY_AGG(customer_id ORDER BY created_at) AS customer_idsFROM customersWHERE email IS NOT NULLGROUP BY emailHAVING COUNT(*) > 1;2. 正規化マッチ(実用の主役)
現場で最も効果が高いのは正規化マッチです。日本企業名は 「株式会社」「(株)」「カブシキガイシャ」「Co., Ltd.」「LLC」 などのバリエーションが多い。これを 正規化関数で吸収してから完全一致をかけます。
import reimport jaconv
def normalize_company_name(s: str) -> str: if not s: return "" # 1. 全角→半角、カタカナ揃え s = jaconv.z2h(s, kana=False, ascii=True, digit=True) s = jaconv.h2z(s, kana=True, ascii=False, digit=False) # 2. 空白除去・大文字化 s = re.sub(r"\s+", "", s).upper() # 3. 法人格を統一 → 削除 legal_forms = [ "株式会社", "(株)", "(株)", "KABUSHIKIKAISHA", "合同会社", "(同)", "(同)", "有限会社", "(有)", "(有)", "CO.,LTD.", "CO.LTD", "CO LTD", "COLTD", "LLC", "LTD", "INC", "K.K.", "KK", ] for lf in legal_forms: s = s.replace(lf, "") # 4. 残った記号を除去 s = re.sub(r"[・\-\.,()()「」『』]", "", s) return s.strip()
assert normalize_company_name("合同会社ふくふく") == "フクフク"assert normalize_company_name("(同)ふくふく") == "フクフク"assert normalize_company_name("ふくふく合同会社") == "フクフク"assert normalize_company_name("Fukufuku LLC") == "FUKUFUKU" # ※カタカナ化は別問題上記の 「全角半角統一 → 大文字化 → 法人格削除 → 記号除去」 の順序は経験則。順序を変えると結果が変わるので、テストケースを書いて固定しましょう。「(株)ABC が ABC になるはずが、`(` を先に消すと `株ABC` のままになる」という事故。
3. ファジーマッチ(タイポ・ 由来のズレ)
正規化しても残るのが タイポ・OCR エラー・スペース挿入:「ふくふく」と「ふくふぐ」、「FUKUFUKU」と「FUKUFLIKU」。Levenshtein 距離()や Jaro-Winkler 類似度 で「ほぼ同じ」を見つけます。
!pip install -q rapidfuzzfrom rapidfuzz import process, fuzz
candidates = ["ふくふく", "ふくふく商会", "ふくふくテック", "ふくふくホールディングス"]query = "ふくふぐ" # タイポ
# 上位 3 件と類似度(0〜100)for match, score, _ in process.extract(query, candidates, scorer=fuzz.WRatio, limit=3): print(f"{match}\t{score:.1f}")# 出力例:# ふくふく 93.3# ふくふく商会 72.0# ふくふくテック 68.5「何点以上を同一とみなすか」は事業によって変わります。85 以上=自動マージ、70〜84=人手レビュー、70 未満=別物、というように 3 段階運用するのが現実的。閾値は過去のマージ実績 を学習データにして調整。
4. ブロック化で N² の爆発を防ぐ
100 万行同士をペア比較すると 10^12 = 1 兆ペア で計算が止まります。ブロック化(Blocking) ── 「同じ郵便番号のなかだけで比較」「会社名の最初の3文字が同じ場合だけ比較」── で N² を N×k に減らすのが定石。
from itertools import combinationsimport pandas as pdfrom rapidfuzz import fuzz
df = pd.read_csv("customers.csv")df["norm_name"] = df["name"].apply(normalize_company_name)
pairs = []for zipcode, group in df.groupby("zipcode"): rows = group.to_dict("records") for a, b in combinations(rows, 2): score = fuzz.WRatio(a["norm_name"], b["norm_name"]) if score >= 85: pairs.append({**{f"a_{k}": v for k, v in a.items()}, **{f"b_{k}": v for k, v in b.items()}, "score": score})
print(f"自動マージ候補: {len(pairs)}件")実務で使う名寄せライブラリ
| ツール | 強み | 弱み |
|---|---|---|
| rapidfuzz (Python) | 高速、ライセンス可、 シンプル | ブロック化は自前 |
| dedupe (Python) | 学習ベース、大規模対応 | 学習データの準備が必要 |
| recordlinkage (Python) | 教科書的アルゴリズムを網羅 | 性能はそこそこ |
| Splink (PySpark) | 数千万行スケール、 | 学習コスト高 |
| OpenRefine () | 対話的にマージ、非エンジニア向け | 数十万行が限界 |
ふくふくの進め方
顧客マスタ・取引先マスタの名寄せ案件は実装の単純な部分と判断の難しい部分が 完全に分かれます。1〜2 ヶ月で「正規化+ファジー+人手レビュー UI」のプロトタイプ、3〜6 ヶ月で運用フロー を整える、というロードマップが現実的。学習ベース(dedupe / Splink) へのアップグレードは、データ規模と精度要求が見えてから判断するのが安全です。
次回予告
EP.07 では、住所の正規化 ── 名寄せの双子の弟。「東京都新宿区西新宿1-2-3」「東京都新宿区西新宿1丁目2-3」「東京都新宿区西新宿1丁目2番3号」を統一する技術を扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。