web-dev-qa-db-ja.com

手続き型コードでのデータ重複排除の効率的なアルゴリズム

私は、大部分がうまく機能するデータクレンジングアプリケーションを作成しました。大量のデータを処理するようには設計されていません。約50万行にすぎません。そのため、設計プロセスの早い段階で、できるだけ多くの作業をインメモリで実行しようとすることが決定されました。データベースやディスクへの書き込みが遅くなると考えられていました。

アプリケーションが提供するさまざまなクリーニング操作のほとんどで、これは真実であることが証明されています。重複排除に関しては、とんでもなく遅いです。かなり強力なサーバーで実行すると、50万行のデータを重複排除するのに約24時間かかります。

私のアルゴリズムは、これらの手順に沿って擬似コードで実行されます。

List<FileRow> originalData;
List<FileRow> copiedData = originalData.Copy;

foreach(FileRow original in originalData)
{
    foreach(FileRow copy in copiedData)
    {
        //don't compare rows against themselves
        if(original.Id != copy.Id) 
        {
            // if it's a perfect match, don't waste time with slow fuzzy algorithm
            if(original.NameData == copy.NameData)
            {
                original.IsDupe = true;
                break;
            }

            // if it's not a perfect match, try Jaro-Winkler
            if(_fuzzyMatcher.DataMatch(original.NameData, copy,NameData))
            {
                original.IsDupe = true;
                break;
            }
        }
    }
}

これを見て、なぜそれがとても遅いのかは明らかです:他の操作が各行を循環できる場合、これは各行に対して再びファイル全体を通過する必要があります。したがって、処理時間は指数関数的に増加します。

また、スピードアップのために他の場所でスレッドを使用しましたが、これをスレッド化する私の試みは失敗しました。実際のコードでは、「true」として複製を作成するだけでなく、それらをグループ化して、特定の一致のすべてのインスタンスが一意のIDを取得できるようにします。ただし、この手順では、別のスレッドが重複を見つけてマークしたかどうかを知る方法がないため、スレッド化により、グループ化IDの割り当てでエラーが発生します。

問題を改善するために、一般的なJaro-Winklerの一致のdbベースのキャッシュを追加して、比較的遅い方法の必要性を排除しました。大きな違いはありませんでした。

私が試すことができる別のアプローチ、またはこのアルゴリズムを高速化するために行うことができる改善点はありますか?それとも、メモリでこれをやろうとしてデータベースに書き込んで、そこで仕事をするのをやめた方がいいですか?

6
Bob Tway

時間の複雑さを本当に変更できる唯一の方法は、アルゴリズムに関連する各値の統計を収集することによって、Jaro-Winklerアルゴリズムを「裏返しに」始めることです。私は正直に言って、あなたの投稿を読む前にこのアルゴリズムについて聞いたことがないことを認めます(ありがとう!)。そのため、漠然としたアイデアを入力し始め、一貫したアプローチに定式化することを期待しています。うまくいけば、これらのアイデアが機能するか、機能する他のいくつかのアイデアを提供します。

したがって、wikiページを見ると、理解する必要があるのは3つしかないようです。

  • 文字列の長さ:s
  • 転置:t
  • 一致する文字:m

各文字列の長さを取得するのは簡単ではありません。各文字列は常に同じです。ただし、転置と一致は各比較に固有です。しかし、2つの文字列が候補ペアであるかどうかを判断するために、これらの正確な値を知る必要はありません。おそらく、絞り込みに役立つテストを作成できると思います。

最初に頭に浮かぶのは、 ブルームフィルター からインスピレーションを得たものです。各文字列に含まれる文字でインデックスを付けるだけです。文字通りすべての文字を取り、それを含む文字列への参照を置きます。

次に、CATなどの文字列を取得して、「C」、「A」、および「T」を含む他のすべての単語を検索できます。それを[C、A、T]と表記します。次に、[C、A]、[C、T]、および[A、T]を検索できます。少しの間、「C」、「A」、または「T」の2つ未満の値はしきい値を満たさないと想定します。つまり、m/s1は1/3未満でなければならないことを知っています。これをもう少し進めて、比較の上限値の計算を開始できます。これは非常に正確である必要はありません。たとえば、[C、A、T]の文字列の場合、次のような上限がある場合があります。

cattywampus: (3/3 + 3/11 + 1)/3 = 0.758
tack: (3/3 + 3/4 + 1)/3 = 0.917
thwack: (3/6 + 2)/3 = 0.833
tachometer: (3/10 + 2)/3 = 0.767

しきい値が.8の場合は、2つを削除できます。これは、2つが実行できる最善の方法が最小値未満であることを知っているためです。 2文字のインデックスの場合、最初の因数m/s1が2/3を超えることはなく、同じ分析を行うことができます。定義上、「C」、「A」、および「T」のいずれも含まない文字列の結果は0です。

これは二次時間からの脱出の始まりだと思います。おそらく、もっと興味深いデータ構造やヒューリスティックを使用すると、もっとうまくできるでしょう。私は誰かがこれらのテクニックのいくつかを提案できると確信しています。十分に定式化されていないアイデアの1つは、これを文字だけでなく文字位置インデックスにもインデックス付けするように拡張することです。

5
JimmyJames

楽しい問題!

私は8年以上前に、データのマージと移行のためにこのようなことをしました。私たちは何十万ものレコードを操作しており、最終的にマージを実行して、スポットチェック可能な結果の完全なセットを分で生成することができました。 ない強力なサーバーでは注意してください。)ファジーマッチングに Levenshtein distance を使用しましたが、コンセプトはほぼ同じです。

基本的に、特定のレコードの候補の一致数を制限できるインデックス可能なヒューリスティックを探します。索引付け可能ルックアップ自体がで動作する必要があるためO(log(n))マージ全体がO(で動作する必要がある場合n * log(n))時間(平均)。

私たちが探したヒューリスティックには2つのタイプがあります。

  • その他のアカウント属性
  • n-grams (フルテキストインデックス、多かれ少なかれ)

まず、物事が複雑になる前に、他の属性をそのまま、または少し変更した状態でグループ化して、小さなクラスターを作成できるかどうかを検討します。たとえば、インデックスを作成してクラスタ化できる信頼できる場所データはありますか?

たとえば、郵便番号データがあり、それらがかなり正確であることがわかっている場合は、そのような問題がすぐに問題を「簡単にする」ことができます。

それ以外の場合、ファジーフルテキストインデックスを作成する必要があります-または少なくともソート

このために、PHP + MySQLを使用してカスタムトライグラムインデックスを作成しました。ただし、その解決策を説明する前に、ここに私の免責事項を示します:

私はこれを8年以上前に行いました。そして、私はそれをよりよく理解したかったので、自分のトライグラムインデックスとランキングアルゴリズムを構築することに真剣に取り組みます。 おそらく、単純なFULLTEXTインデックスを利用するか、 Sphinx のような既成の検索エンジンを使用して同じ結果を得ることができます。より良くない場合。

とは言っても、基本的なfunソリューションは次のとおりです。

  1. 各レコード
    1. インデックス検索列の長さ(重複除外ジョブの特別な最適化!)
    2. Nグラムを抽出する
    3. 抽出された各Nグラムについて
      • N-gramをngram_recordテーブル(またはコレクション)に追加する
  2. N-gram統計を生成する
    1. ngram_record
      • Ngramカーディナリティを初期化または更新する
    2. カーディナリティの中央値を計算する
    3. 標準偏差を計算する
    4. ngram ごとに
      • 関連性を、分布に対するカーディナリティの関数として割り当てます。
  3. レコードごと(source
    1. 上位のN最も関連性の高いngramを見つける
    2. 各ngram
      • Ngramを含むレコードを検索する
      • 長さ+/- C/source長さのレコードをフィルタリング
      • 関連性の割り当て= ngramの関連性
    3. IDで候補をグループ化し、関連性を合計する
    4. 関連性の降順で並べ替え
    5. Mの最も関連性の高い候補に対してファジー文字列距離マッチングを実行する

このソリューションは機能的ですが、非常に「未熟」です。ロットには最適化と拡張の余地があります。たとえば、検索フィールドが文字ngramに分解される前に単語ngramに分解される場合は、追加のインデックスレイヤーを使用できます。

私が行ったもう1つの最適化は、完全に成熟したわけではなく、ngram_record関係に関連性を割り当てることでした。特定のngramを検索する場合、「ソース」レコードと「候補」レコードの間でngramが類似した関連性を持つレコードを選択できれば、より良い結果が得られます。関連性は関数ngram関連性であり、レコード内での頻度です、およびレコードの長さ。

上記のNMCの値を調整する余地もたくさんあります。

楽しんでください!


または Sphinx を使用します。 真剣に、楽しみたくない場合は、全文検索エンジンを見つけて、それを使用してください。

そして結局のところ、私は実際に数年前と同様にあいまい検索の質問に答えました。参照: 何百万ものレコードで部分的に一致する名前

2
svidgen

問題

主な問題は、O(n ^ 2)逐次アルゴリズムです。 500.000行の場合、250.000.000.000回の反復です。実行に24時間かかる場合、それは反復あたり300ナノ秒を意味します。

すぐに小さな改善

リストのすべてのアイテムを他のアイテムと比較しますが、2回実行します。最初に外部ループのaを内部ループのbと比較し、次にbは外側のループでaは内側のループで。

Jaro-Winklerメトリックは symmetric なので、比較を2回行う必要はありません。内側のループでは、残りの要素を反復処理するだけで十分です。この改善は並外れたものではありません。それでもO(n ^ 2)ですが、少なくとも実行時間が2分の1になります。

余談ですが、言語によっては、メインの問題と比較して大幅に改善されない場合でも、リストに関連するパフォーマンスの問題が発生する可能性があります(たとえば、 C#リストについてはこちらを参照してください) foreach 、またはC++の大きなリストのメモリ割り当て/割り当て解除のオーバーヘッドを考慮してください)。

インメモリソリューション?

正確なデュープを見つける必要があるだけなら、はるかに高速な方法は次のとおりです。

  • 行を反復処理するときにいくつかのハッシュコードを計算し、ハッシュコードを一致する行のリストに関連付けるマップを入力します。
  • 最初のパスの後、マップを反復処理し、単一の要素のリストを持つエントリを無視し、複数の要素(識別される可能性のあるグループ)を含むリストを処理できます。同じハッシュが常に同じ値です)。

このアルゴリズムの複雑さは、O(2n)であるため、最良の場合は100万回の反復であり、仮想の最悪の場合はO(n ^ 2)です(すべての行が単一の重複であるため) )実際のケースは重複グループの数とこれらの各グループの要素の数に依存しますが、これはあなたのアプローチよりも桁違いに速いと期待しています。

一致が「正規化」関数f()によって定義され、f(record1)==f(record2)が一致があることを意味する場合、あいまい一致は同じ方法で簡単に実装できます。これは、たとえば、あいまい一致が soundex のバリアントに基づいている場合に機能します。

残念ながら、 Jaro Winklerの距離 はこの要件を満たしていないため、すべての行を互いに比較する必要があります。

データベースソリューション

直感的には、特にあいまい一致が少し複雑な場合やフィールドを操作する場合は、DBMSアプローチを使用することもできます。

サーバーが適切にディメンション化されている場合(シングルパスバルクアップロード)、DBMSに50万行を設定する場合、原則として24時間をはるかに下回るはずです。ソートされたSELECTまたはGROUP BY句を使用すると、正確な重複を簡単に見つけることができます。 「正規化」機能を持つファジーマッチについても同様です。

しかし、Jaro-Winklerのように明示的な比較を必要とするあいまい一致の場合、あまり役に立ちません。

Divide et impera variant

メトリックが行全体ではなく一連のフィールドに適用されている場合、DBMSはフィールドレベルで作業することで比較の数を減らすことができます。アイデアは、それらの間のすべてのレコードの比較を回避することですが、組み合わせ爆発の影響が妥当な範囲に留まっている小さなサブセットのみを考慮します。

  • 関連するフィールドで、一意の値を選択します。これらはしばしば小さなセットを形成します。
  • 可能性のあるグループを識別するために、この小さいセットでメトリックを計算します
  • 近接性が不十分なペア値を無視する

次の例では、5ではなく3つの値のみを比較します。

George
Melanie  
Georges 
George  
Melanie  

その結果、しきい値は85%になります。

George  / Georges    97%   (promising)
George  / Melanie    54%   (ignored)
Melanie / Georges    52%   (ignored)

複数のフィールドが関係している場合は、各フィールドを個別に処理して、潜在的な有望な一致するサブグループを識別します。例えば:

George    NEW-YORK
Melanie   WASHINGTON
Georges   NEW IORK
George    OKLAHOMI
Melanie   OKLAHOMA

非有望な値を削除した後、候補グループの2番目のリストを追加します):

NEY-YORK / NEW IORK
OKLAHOMAI / OKLAHOMA 

次に、有望なグループの各関連フィールドのすべての値を持つレコードを選択します。ここでは、{ジョージ、ジョージ}と{ニューヨーク、ニューヨーク、オクラホマ、オクラホマ}です。返される唯一のレコードは次のとおりです。

George    NEW-YORK
Georges   NEW IORK
George    OKLAHOMI

次に、2つの戦略があります。

  • サブセットが十分に削減されている場合は、選択したレコードに対してアルゴリズムを実行します。
  • または、潜在的なサブグループ値に対応するレコードのみですべてのレコードを検索することにより、マッチングを高速化します(これは、スペースを犠牲にして、各フィールドのサブグループのヘッドでの一種のタグ付けを想像できます)。

2番目のアプローチの結果は次のようになります。

  selected values           field group tag
-------------------        ------------------
George    NEW-YORK    ->   George NEW-YORK
Georges   NEW IORK    ->   George NEW-YORK
George    OKLAHOMI    ->   George OKLAHOMA

その後、もちろん、複数のレコードを持つグループのみを考慮して、タグでGROUP BYを選択してグループを作成します。

1
Christophe