web-dev-qa-db-ja.com

Pythonでのファジー文字列マッチング

命名規則が少し異なる100万を超える名前の2つのリストがあります。ここでの目標は、95%の信頼度のロジックを使用して、類似するレコードを一致させることです。

PythonのFuzzyWuzzyモジュールなど、活用できるライブラリがあることを知っています。

ただし、処理に関しては、1つのリスト内のすべての文字列が他のリストと比較するにはリソースを大量に消費するようです。この場合、100万倍の数の反復が必要になるようです。

この問題に対して他のより効率的な方法はありますか?

更新:

だから私はバケット関数を作成し、空白、記号を削除し、値を小文字などに変換する単純な正規化を適用しました...

for n in list(dftest['YM'].unique()):
    n = str(n)
    frame = dftest['Name'][dftest['YM'] == n]
    print len(frame)
    print n
    for names in tqdm(frame):
            closest = process.extractOne(names,frame)

Pythons pandasを使用することにより、データは年単位でグループ化された小さなバケットに読み込まれ、FuzzyWuzzyモジュールprocess.extractOneは、最適な一致を得るために使用されます。

結果はいまだにがっかりです。テスト中、上記のコードは5000の名前のみを含むテストデータフレームで使用され、ほぼ1時間かかります。

テストデータは分割されます。

  • 名前
  • 生年月日

そして、それらのYMが同じバケット内にあるバケットでそれらを比較しています。

問題は私が使用しているFuzzyWuzzyモジュールが原因である可能性がありますか?どんな助けにも感謝します。

14
BernardL

この問題をO(n ^ 2)から時間の複雑さを軽減するために、いくつかのレベルの最適化が可能です。

  • 前処理:最初のパスでリストをソートし、各文字列の出力マップを作成します。マップのキーは正規化された文字列です。正規化には次のものがあります。

    • 小文字変換、
    • 空白なし、特殊文字の削除、
    • ユニコードを可能な限りASCIIに変換します nicodedata.normalize または nidecode moduleを使用します)

    これは"Andrew H Smith""andrew h. smith""ándréw h. smith"同じキーを生成"andrewhsmith"を使用すると、100万の名前のセットが、一意の/類似したグループ化された名前の小さなセットに削減されます。

この tlity method を使用して文字列を正規化できます(ただし、Unicode部分は含まれません)。

def process_str_for_similarity_cmp(input_str, normalized=False, ignore_list=[]):
    """ Processes string for similarity comparisons , cleans special characters and extra whitespaces
        if normalized is True and removes the substrings which are in ignore_list)
    Args:
        input_str (str) : input string to be processed
        normalized (bool) : if True , method removes special characters and extra whitespace from string,
                            and converts to lowercase
        ignore_list (list) : the substrings which need to be removed from the input string
    Returns:
       str : returns processed string
    """
    for ignore_str in ignore_list:
        input_str = re.sub(r'{0}'.format(ignore_str), "", input_str, flags=re.IGNORECASE)

    if normalized is True:
        input_str = input_str.strip().lower()
        #clean special chars and extra whitespace
        input_str = re.sub("\W", "", input_str).strip()

    return input_str
  • これで、正規化されたキーが同じである場合、類似の文字列はすでに同じバケットに配置されます。

  • さらに比較するには、名前だけではなく、キーだけを比較する必要があります。例:andrewhsmithandrewhsmeeth。名前のこの類似性には、上記の正規化された比較とは別に、あいまいな文字列照合が必要になるためです。

  • Bucketing本当に5文字のキーを9文字のキーと比較して、それが95%かどうかを確認する必要がありますか?一致?いいえ、必要ありません。したがって、文字列に一致するバケットを作成できます。例えば5文字の名前は4-6文字の名前、5-7文字の6文字の名前などと照合されます。n文字キーのn + 1、n-1文字の制限は、最も実用的な照合に適したバケットです。

  • 最初の一致:名前のほとんどのバリエーションは、正規化された形式で最初の文字が同じになります(例:Andrew H Smithándréw h. smithAndrew H. Smeethキーandrewhsmithandrewhsmith、およびandrewhsmeethをそれぞれ生成します。通常、最初の文字に違いはないため、aで始まるキーとaで始まる他のキーを照合し、長さバケット内に収めることができます。これにより、マッチング時間が大幅に短縮されます。キーandrewhsmithbndrewhsmithに一致させる必要はありません。そのため、最初の文字を含む名前のバリエーションはほとんど存在しません。

次に、この method (またはFuzzyWuzzyモジュール)の行で何かを使用して、文字列の類似性のパーセンテージを検索できます。速度と結果を最適化するために jaro_winkler またはdifflibのいずれかを除外できます品質:

def find_string_similarity(first_str, second_str, normalized=False, ignore_list=[]):
    """ Calculates matching ratio between two strings
    Args:
        first_str (str) : First String
        second_str (str) : Second String
        normalized (bool) : if True ,method removes special characters and extra whitespace
                            from strings then calculates matching ratio
        ignore_list (list) : list has some characters which has to be substituted with "" in string
    Returns:
       Float Value : Returns a matching ratio between 1.0 ( most matching ) and 0.0 ( not matching )
                    using difflib's SequenceMatcher and and jellyfish's jaro_winkler algorithms with
                    equal weightage to each
    Examples:
        >>> find_string_similarity("hello world","Hello,World!",normalized=True)
        1.0
        >>> find_string_similarity("entrepreneurship","entreprenaurship")
        0.95625
        >>> find_string_similarity("Taj-Mahal","The Taj Mahal",normalized= True,ignore_list=["the","of"])
        1.0
    """
    first_str = process_str_for_similarity_cmp(first_str, normalized=normalized, ignore_list=ignore_list)
    second_str = process_str_for_similarity_cmp(second_str, normalized=normalized, ignore_list=ignore_list)
    match_ratio = (difflib.SequenceMatcher(None, first_str, second_str).ratio() + jellyfish.jaro_winkler(unicode(first_str), unicode(second_str)))/2.0
    return match_ratio
16
DhruvPathak

O(n ^ 2)の実行を回避するには、文字列をインデックス化するか、文字列を正規化する必要があります。基本的に、各文字列を通常のフォームにマップし、対応する通常のフォームにリンクされたすべての単語を含む逆引き辞書を構築する必要があります。

「世界」と「言葉」の通常の形は同じであると考えてみましょう。したがって、最初にNormalized -> [Word1, Word2, Word3],の逆引き辞書を作成します。例:

"world" <-> Normalized('world')
"Word"  <-> Normalized('wrd')

to:

Normalized('world') -> ["world", "Word"]

これで、正規化された辞書の複数の値を持つすべてのアイテム(リスト)が一致した単語になります。

正規化アルゴリズムは、データ、つまり単語に依存します。多くの1つを検討してください:

  • Soundex
  • メタフォン
  • ダブルメタフォン
  • NYSIIS
  • Caverphone
  • ケルン音声学
  • MRAコーデックス
4
Zaur Nasibov

Fuzzywuzzyに固有であることに注意してください。現在、process.extractOneのデフォルトはWRatioで、これはアルゴリズムの中で最も遅いことに注意してください。プロセッサはデフォルトでutils.full_processになっています。スコアラーとして言うfuzz.QRatioを渡すと、はるかに速くなりますが、何を一致させようとしているのかによってはそれほど強力ではありません。名前だけでも大丈夫かもしれません。私は個人的には、WRatioより少なくともいくらか速いtoken_set_ratioで頑張っています。また、事前にすべての選択に対してutils.full_process()を実行し、スコアラーとしてfuzz.ratioを使用して実行し、processor = Noneで処理ステップをスキップすることもできます。 (以下を参照)基本的な比率関数を使用しているだけの場合、fuzzywuzzyはおそらくやりすぎです。 Fwiw私はJavaScriptポート(fuzzball.js)を持っているので、トークンセットを事前に計算して、毎回再計算する代わりにそれらを使用することができます。)

これにより、比較の数が減るわけではありませんが、役立ちます。 (おそらくこれのためのBKツリー?同じ状況を自分で調べています)

より高速な計算を使用できるように、python-Levenshteinがインストールされていることも確認してください。

**以下の動作は変更される可能性があり、議論中の問題が開かれます**

fuzz.ratioは完全なプロセスを実行せず、token_setおよびtoken_sort関数はfull_process = False paramを受け入れます。Processor= Noneを設定しない場合、extract関数はとにかく完全なプロセスを実行しようとします。 functoolsのパーシャルを使用して、full_process = Falseをスコアラーとしてfuzz.token_set_ratioに渡すと言うことができ、事前に選択内容でutils.full_processを実行できます。

2
nbkap