0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

3つのスクリプトを統合して作った高精度名寄せツール

Last updated at Posted at 2025-10-17

3つのスクリプトを統合して作った高精度名寄せツール

はじめに

大規模なデータクレンジングの過程で、表記ゆらぎのある商品名を標準名に統一する「名寄せ」作業に苦戦していました。試行錯誤の末、レーベンシュタイン距離とJaccard係数を組み合わせたハイブリッドスコアリングで割と精度の高い名寄せを実現できたので、その内容を書きます。
※今回のデータクレンジングで様々なPythonコードを作ったのでそれぞれ別々に公開していきます。

作ったもの

Comprehensive Data Matcher - 高精度な名寄せツール

主な機能

  • レーベンシュタイン距離とJaccard係数のハイブリッドスコアリング
  • 全角/半角、語順の違い、同義語などの表記ゆらぎに対応
  • Excel形式の入出力
  • リアルタイムログ出力

なぜ作ったのか

直面していた課題

大規模なデータクレンジングプロジェクトで、こんな問題に直面していました:

入力データ(表記ゆらぎだらけ)

オーバーサイズ コットンTシャツ 白
ビッグシルエット 綿Tシャツ ホワイト
デニム スリムフィット ブルー パンツ
パーカー フーディー 黒 スウェット

マスターデータ(標準名)

オーバーサイズ コットンTシャツ 白
スリムフィット デニムパンツ ブルー
フード付き スウェットパーカー 黒

これらを自動でマッチングしたい、でも、単純な文字列一致では厳しいか。

既存手法の限界

最初は単純なレーベンシュタイン距離だけでやってみましたが:

うまくいかなかったケース

  • 「デニム スリムフィット ブルー パンツ」と「スリムフィット デニムパンツ ブルー」
    → 語順が違うだけなのにスコアが低い...

別の問題

  • 「ビッグシルエット」と「オーバーサイズ」
    → 意味は同じなのに、文字レベルでは全然違う...

もっと賢いアルゴリズムが必要だと思い、試行錯誤が始まりました。

解決策: ハイブリッドスコアリング方式

アイデアの着想

ふと、人間が名寄せするときもこんな感じかなと:

  1. 文字レベルで似てるか?

    • 「オーバーサイズTシャツ」と「オーバーサイズ Tシャツ」
    • → スペース1個の違いだけ!同じだ!
  2. 単語レベルで一致してるか?

    • 「スリムフィット デニムパンツ」と「デニムパンツ スリムフィット」
    • → 順番は違うけど、使ってる単語は同じ!同じだ!

じゃあ、この2つを組み合わせればいいんじゃない?

アルゴリズムの選定

調べた結果、この2つを使うことにしました:

1. レーベンシュタイン類似度(文字レベル)

特徴

  • 編集距離(Edit Distance)ベース
  • 何文字変更すれば一致するかを計算

得意なケース

"オーバーサイズTシャツ" vs "オーバーサイズ Tシャツ"
 スペース1個の挿入だけ  類似度: 91.7%

実装

from rapidfuzz import fuzz

lev_score = fuzz.ratio(str1, str2) / 100.0  # 0.0-1.0に正規化

2. Jaccard係数(単語レベル)

特徴

  • 集合の類似度を測定
  • 語順に依存しない

得意なケース

"スリムフィット デニムパンツ" vs "デニムパンツ スリムフィット"
 単語の集合は同じ  Jaccard係数: 1.0 (100%)

実装

def tokenize(s: str) -> set:
    """文字列を単語に分割"""
    tokens = re.split(r'[  /]', s)
    return set(filter(None, tokens))

def jaccard_score(s1: str, s2: str) -> float:
    tokens1 = tokenize(s1)
    tokens2 = tokenize(s2)
    intersection = len(tokens1.intersection(tokens2))
    union = len(tokens1.union(tokens2))
    return intersection / union if union > 0 else 0.0

ハイブリッドスコアの計算式

2つのスコアを重み付けして組み合わせます:

ハイブリッドスコア = (レーベンシュタイン類似度 × 0.7) + (Jaccard係数 × 0.3)

なぜこの重み?

  • 文字レベルの類似度を優先(0.7)
  • でも語順の違いにも対応したい(0.3)
  • 実験の結果、この比率が最もバランスが良かった

計算例

入力: "デニム スリムフィット ブルー パンツ"
候補: "スリムフィット デニムパンツ ブルー"

レーベンシュタイン類似度 = 0.75  # 文字の並びは少し違う
Jaccard係数 = 1.0                # 単語は完全一致

ハイブリッドスコア = (0.75 × 0.7) + (1.0 × 0.3)
                 = 0.525 + 0.3
                 = 0.825 (82.5%) 

単純なレーベンシュタインだけなら75%だったのが、ハイブリッドで82.5%に!

実装の工夫

1. NFKC正規化

マッチング前に、文字を正規化して表記の違いを吸収:

import unicodedata

def normalize_cell_nfkc(value):
    if isinstance(value, str):
        return unicodedata.normalize('NFKC', value)
    return value

効果

"オーバーサイズ" (全角) → "オーバーサイズ"
"オーバーサイズ" (半角カナ) → "オーバーサイズ"

2. 大文字化による正規化

def normalize_for_comparison(s: str) -> str:
    return s.upper()  # 大文字化

効果

"Tshirt" → "TSHIRT"
"tshirt" → "TSHIRT"
"TShirt" → "TSHIRT"

3. キャッシュによる高速化

標準商品名リストを事前に正規化してキャッシュ:

correct_cache = [
    {"original": s, "normalized": normalize_for_comparison(s)}
    for s in correct_names
]

これで毎回正規化する必要がなくなり、大幅に高速化!

4. リアルタイムログ出力

処理の進捗をリアルタイムで確認できるように:

result_message = (
    f"[マッチング結果] 「{original}」=>「{matched}"
    f"(Hybrid: {hybrid_score:.2f}, "
    f"Lev: {lev_score:.2f}, "
    f"Jac: {jac_score:.2f})"
)
logger.info(result_message)

出力例

[2025-04-30 10:00:05] [INFO] [マッチング結果] 「オーバーサイズ コットンTシャツ 白」=>「オーバーサイズ コットンTシャツ 白」 (Hybrid: 0.97, Lev: 0.95, Jac: 1.00)

結果

マッチング精度

サンプルデータ(約30件)で検証した結果:

ゆらぎの種類 マッチング精度
文字種の違い(全角/半角) 95%以上
語順の違い 95%以上
省略形 95%以上

パフォーマンス

データ件数: 1,000件のゆらぎデータ
標準名: 100件
処理時間: 約3-5秒

かなり実用的な速度!

開発の変遷

実は、最初から今の形だったわけじゃなくて、3つの別々のスクリプトから始まったんです。

v1.0: 3つの独立したスクリプト

convert_fullwidth_to_halfwidth.py

  • 全角/半角の変換
  • 中間ファイルに出力

Levenshtein.py

  • レーベンシュタイン距離でマッチング
  • 中間ファイルから読み込み、中間ファイルに出力

Levenshtein_merge.py

  • 最終的な結合処理
  • 結果ファイルに出力

問題点

  • 3つのスクリプトを順番に実行する必要がある
  • 中間ファイルが2つも生成される
  • 処理が遅い
  • メンテナンスが大変

v2.0: 統合と最適化

「これ、1つにまとめた方が絶対いいよね...」と思い、結合:

改善点

  • 3つのスクリプトを1つに統合
  • 中間ファイルを廃止(すべてメモリ上で処理)
  • 処理フローの最適化
  • コードの可読性向上

結果

  • 実行時間短縮
  • ファイル管理が楽に
  • デバッグしやすくなった

v2.1: リアルタイムログ追加

処理中に何が起こってるか見たいので:

logger.info(f"処理中: {i + 1}/{total_count}")
logger.info(f"[マッチング結果] 「{original}」=>「{matched}")

これで処理の進捗が目で見えるように

v2.2: ハイブリッドスコアリング導入(現在)

レーベンシュタイン距離だけでは限界を感じたので、Jaccard係数を追加:

追加機能

  • ハイブリッドスコアリング方式
  • スコアの重み調整機能
  • 各スコアの内訳を出力

結果

  • マッチング精度が約15%向上
  • より複雑なゆらぎに対応可能に

OSS化した理由(OSSと言えるほどのものではありませんが……)

なぜ公開したのか

  1. 同じ悩みを持つ人の役に立ちたい

    • データクレンジングってみんな苦労してる
    • 試行錯誤の成果を共有したい
  2. フィードバックが欲しい

    • もっといいアルゴリズムがあるかも?
    • 他の人の知見も取り入れたい
  3. ポートフォリオとして

    • 実務で培った技術力を示せる
    • コードの書き方やドキュメント作成の練習になる

公開前の準備

GitHub公開のために以下を整備:

ドキュメント

  • README.md(日本語版)
  • アルゴリズム詳細説明
  • セットアップガイド
  • 貢献ガイドライン
  • 変更履歴

サンプルデータ

  • 実データは使えないので架空のデータで作成
  • 複雑な名寄せができることを示すサンプル

GitHub設定

  • Issue/PRテンプレート
  • GitHub Actions(CI/CD)
  • ライセンス(MIT)

使い方

インストール

git clone https://github.com/Yuki-M0906/comprehensive-data-matcher.git
cd comprehensive-data-matcher
pip install -r requirements.txt

基本的な使い方

  1. 入力ファイルを準備

yuragi.xlsx:

商品名
オーバーサイズ コットンTシャツ 白
デニム スリムフィット ブルー パンツ

correct.xlsx:

商品名
オーバーサイズ コットンTシャツ 白
スリムフィット デニムパンツ ブルー
  1. 実行
python comprehensive_data_matcher.py
  1. 結果を確認

result_combined_high_accuracy.xlsx:

元のゆらぎ名 マッチした正規名 ハイブリッドスコア
オーバーサイズ コットンTシャツ 白 オーバーサイズ コットンTシャツ 白 0.967

カスタマイズ

スコアの重みを調整して、自分のデータに最適化できます:

# 誤字脱字が多い場合
WEIGHT_LEVENSHTEIN = 0.8
WEIGHT_JACCARD = 0.2

# 語順の違いが多い場合
WEIGHT_LEVENSHTEIN = 0.5
WEIGHT_JACCARD = 0.5

今後の展望

実装したい機能

  1. CSV形式のサポート

    • Excelだけじゃなくて、CSVも使えるように
  2. コマンドライン引数対応

    python comprehensive_data_matcher.py --yuragi data.xlsx --correct master.xlsx --output result.xlsx
    
  3. 設定ファイル対応

    • YAMLやJSONで設定を外出し
    • プロジェクトごとに設定を保存
  4. 機械学習による重み最適化

    • 過去のマッチング結果から最適な重みを自動学習
    • データセットに応じて自動調整
  5. Web UI

    • ブラウザから使えるように
    • ドラッグ&ドロップでファイルアップロード

コミュニティからの期待

もしこのツールに興味を持ってくれたら:

  • Star してもらえると嬉しいです!
  • バグ報告や機能リクエストもお待ちしてます
  • プルリクエストも大歓迎!

参考文献・技術スタック

使用技術

  • Python 3.8+
  • pandas: データ処理
  • openpyxl: Excel操作
  • rapidfuzz: 高速文字列マッチング

参考にした論文・資料

  • Levenshtein, V. I. (1966). "Binary codes capable of correcting deletions, insertions, and reversals"
  • Jaccard, P. (1912). "The distribution of the flora in the alpine zone"
  • rapidfuzz Documentation

学んだこと

技術面

  1. 単一のアルゴリズムには限界がある

    • 複数のアプローチを組み合わせることで、より柔軟なソリューションに
  2. 前処理の重要性

    • NFKC正規化を事前に組み入れるだけで精度が大幅に向上
    • データクレンジングは前処理が8割
  3. パフォーマンスチューニング

    • キャッシュの活用
    • 不要な中間ファイルの削除

プロジェクト管理面

  1. ドキュメントの重要性

    • 良いドキュメントがあると使ってもらいやすい
    • 自分が後で見返すときも助かる
  2. バージョン管理

    • CHANGELOGを書くと、何を改善したか明確に
  3. OSS化の意義

    • 知識の共有は自分の成長にもつながる
    • コミュニティからのフィードバックが学びになる

まとめ

データクレンジングの課題から始まり、あまり知識がない私としては試行錯誤を経て、レーベンシュタイン距離とJaccard係数を組み合わせたハイブリッドスコアリング方式にたどりつきました。

その場しのぎ的に作った3つのスクリプトを統合し、機能を追加していく中で、単なるツールから「使いやすいツール」へと成長させることができました。

このツールが役立ちそうな場面

  • 商品名の名寄せ
  • 顧客データの統合
  • 住所の正規化
  • その他、表記ゆらぎのあるデータのマッチング

もし同じような課題に直面している方がいたら、ぜひ使ってみてください!
フィードバックや改善提案もお待ちしています。

リンク


最後まで読んでいただき、ありがとうございました!
質問やコメントがあれば、GitHubのIssueやこの記事のコメント欄でお気軽にどうぞ!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?