web-dev-qa-db-fra.com

Algorithme efficace pour la déduplication de données dans le code de procédure

J'ai écrit une application de nettoyage de données qui, pour la plupart, fonctionne bien. Il n'est pas conçu pour gérer de grands volumes de données: rien de plus d'environ un demi-million de lignes. Donc tôt dans le processus de conception, une décision a été prise pour essayer de faire autant que possible le travail dans la mémoire. La pensée était que l'écriture de la base de données ou du disque ralentirait les choses.

Pour la plupart des diverses opérations de nettoyage, les offres de l'application, cela s'est avéré vrai. En ce qui concerne la déduplication, mais il est absurdement lent. En cours d'exécution sur un serveur assez puissant, il faut environ 24 heures pour déduire une demi-million de données de données.

Mon algorithme fonctionne selon ces étapes de pseudocode:

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;
            }
        }
    }
}

Regardez ceci, c'est évident pourquoi il est si lent: où d'autres opérations peuvent parcourir chaque ligne, cela doit parcourir l'ensemble du fichier pour chaque rangée. Donc, le temps de traitement augmente de manière exponentielle.

J'ai également utilisé le filetage ailleurs pour accélérer les choses, mais mes attestations à enfiler cela ont échoué. Dans le code du monde réel, nous ne faisons que fabriquer des doublons comme "vrai", mais les grouper, de sorte que toutes les instances d'un match donné obtiennent une pièce d'identité unique. Mais la procédure n'a aucun moyen de savoir si un autre thread a trouvé et marqué un duplicata, de sorte que le filetage conduit à des erreurs dans l'attribution d'identification de regroupement.

Pour essayer d'améliorer les choses, nous avons ajouté un cache basé sur la DB des matchs communs Jaro-Winkler pour essayer d'éliminer la nécessité de cette méthode relativement lente. Cela n'a pas fait de différence significative.

Y a-t-il une autre approche que je peux essayer ou des améliorations que je peux apporter à cet algorithme pour la rendre plus rapide? Ou suis-je préférable d'abandonner essayer de faire cela en mémoire et d'écrire dans une base de données pour faire le travail là-bas?

6
Bob Tway

Il me semble que la seule façon dont vous pouvez vraiment changer la complexité de temps consiste à commencer à transformer l'algorithme Jaro-Winkler 'insidieux "en collectant des statistiques sur chaque valeur pertinente pour l'algorithme. Je serai honnête et admet que j'avais jamais entendu parler de cet algorithme avant de lire votre message (merci!). En tant que tel, je vais commencer à taper des idées vagues et j'espère que le formulateur dans une approche cohérente. Espérons que ces idées avec votre travail ou vous donneront d'autres idées qui font.

Donc, en regardant la page wiki, il semble qu'il n'y ait que trois choses que vous devez comprendre:

  • longueur des cordes: s
  • transpositions: t
  • caractères correspondants: m

Obtenir la longueur de chaque chaîne est plus facile. C'est toujours la même chose pour chaque chaîne: fait. Mais les transpositions et les allumettes sont spécifiques à chaque comparaison. Mais nous n'avons pas nécessairement de connaître des valeurs exactes pour celles-ci afin de déterminer si deux chaînes sont une paire de candidats. Je pense que vous pouvez probablement créer des tests qui aideront à réduire les choses.

La première chose qui vient à l'esprit est inspirée par Filtre Bloom : Indexez simplement chaque chaîne par les caractères qu'il contient. Prenez littéralement toutes les lettres et mettez une référence aux cordes qui le contiennent.

Ensuite, je peux prendre une corde comme chat et trouver tous les autres mots contenant un 'C', 'A' et 'T'. Je vais noter que comme [C, A, T]. Je peux ensuite rechercher [C, A], [C, T] et [A, T]. Je présume un instant que quelque chose avec moins de deux de "C", A "ou" T "ne respecterait pas le seuil I.e. Vous savez que M/S1 doit être inférieur à 1/3. Vous pouvez prendre cela un peu plus loin et commencer à calculer une valeur limitée supérieure pour la comparaison. Ce n'est pas nécessaire d'être très précis. Par exemple, avec les cordes de [C, A, T], vous pourriez avoir ces limites supérieures:

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

Si votre seuil est .8, vous pouvez éliminer deux d'entre eux parce que vous savez que les meilleurs qu'ils puissent faire est de votre minimum. Pour l'indice de deux lettres, vous savez que le premier facteur M/S1 ne peut jamais être supérieur à 2/3 et vous pouvez faire la même analyse. Toute chaîne qui n'a pas de "C", "A" et "T" a le résultat de 0, par définition

Je pense que ceci est un début d'éloigner du temps quadratique. Vous pouvez probablement mieux faire mieux avec des structures de données ou des heuristiques plus intéressantes. Je suis sûr que quelqu'un peut suggérer certaines de ces techniques. Une idée non bien formulée consiste à étendre ceci à indexer non seulement les caractères mais également à un index de position de caractère.

5
JimmyJames

problème amusant!

J'ai fait ce genre de chose pour une fusée de données et une migration il y a plus de 8 ans. Nous opérions de l'ordre des centaines de milliers de documents et nous étions finalement capables d'exécuter la fusion pour produire un ensemble complet de résultats optimaux sur place par rapport à minutes ( non sur un serveur puissant, pensez-vous.) Nous avons utilisé une Distance Levenshtein pour notre correspondance floue, mais le concept ressemble à la même chose.

Essentiellement, vous souhaitez rechercher des heuristiques indexables pouvant limiter le nombre de matchs candidats à tout enregistrement donné. indexable car la recherche elle-même doit fonctionner dans O (journal (n)) Si toute la fusion doit fonctionner dans O (N * journal (n)) heure (en moyenne).

Il y a deux types d'heuristiques que nous avons recherchées:

  • Autres attributs de compte
  • N-grammes (indexation du texte complet, plus ou moins)

Tout d'abord, Avant que les choses ne soient compliquées, déterminez si d'autres attributs que vous pouvez regrouper comme-ils-sont ou dans un état légèrement modifié pour créer des grappes plus petites. Par exemple, est-il fiable Emplacement Données que vous pouvez indexer et cluster sur?

Si vous avez des données de code postal, par exemple, et vous les connaissez raisonnablement exactes, quelque chose comme peut "banaliser" le problème juste là.

Sinon, Vous devez construire un index flou-texte intégral - ou au moins Sorte de.

Pour cela, j'ai écrit un indice de trigramné personnalisé avec PHP + mysql. Mais, avant d'expliquer cette solution, Voici mon avertissement de non-responsabilité:

J'ai fait cela il y a plus de 8 ans. Et, j'en mettit directement dans la construction de mon propre index de Trigram et de mon algorithme de classement parce que je voulais mieux le comprendre. Vous pouvez probablement tirer un simple index FULLTEXT ou utiliser un -Le moteur de recherche comme - sphinx pour obtenir les mêmes résultats, sinon mieux.

Cela dit, voici la solution amusement Solution:

  1. Pour chaque enregistrement
    1. Longueur de la colonne d'index (une optimisation spéciale pour les travaux DEUPE!)
    2. Extraire n-grammes
    3. Pour chaque n-gram extrait
      • Ajouter N-gram à un ngram_record table (ou collection)
  2. Générer des statistiques N-GRAM
    1. Pour chaque ngram_record
      • Initialiser ou mettre à jour ngram cardinalité
    2. Calculer la cardinalité médiane
    3. Calculer la déviation standard
    4. Pour chaque ngram [.____]
      • Assigner Pertinence en fonction de la cardinalité contre la distribution
  3. Pour chaque enregistrement (source) [.____]
    1. Trouver top N ngams les plus pertinents
    2. Pour chaque ngram
      • Rechercher des enregistrements contenant le ngram
      • Filtre pour enregistrements avec longueur +/- C de source longueur
      • Attribuer la pertinence = Ngram pertinence
    3. Candidats de groupe par ID, Sommation Pertinence
    4. Ordre par pertinence décroissant
    5. Effectuer une correspondance de chaîne floue contre les candidats les plus pertinents M

Notez que cette solution est fonctionnelle, mais très "immature". Il y a un lot de la place d'optimisation et d'amélioration. Une couche supplémentaire d'indexation pourrait être utilisée, par exemple, où le champ de recherche est cassé en ngams de mots avant la rupture des ngrams de caractères.

Une autre optimisation que j'ai faite, mais elle n'a pas été entièrement mature, cédait de la pertinence à travers le ngram_record des relations. Lorsque vous recherchez un ngam donné, vous obtiendrez de meilleurs résultats si vous pouvez sélectionner des enregistrements pour lesquels le NGRAM a une pertinence similaire entre les enregistrements "Source" et "Candidate", où la pertinence est une pertinence de Ngram de fonction, sa fréquence dans l'enregistrement. et la longueur de l'enregistrement.

Il y a aussi beaucoup de place pour modifier votre N, M et C valeurs ci-dessus.

Amusez-vous!


ou utiliser Sphinx . Sérieusement, si vous n'allez pas vous amuser, trouvez un moteur de recherche complet et Utilisez cela.

Et comme il s'avère, j'ai répondu à une question de recherche floue de la même manière il y a quelques années. Voir: Nom partiel correspondant à des millions d'enregistrements .

2
svidgen

Le problème

Le principal problème est votre O (n ^ 2) algorithme séquentiel. Pour les lignes des thats 500.000 250.000.000.000 itérations. Si cela prend 24 heures pour l'exécuter, cela signifie que 300 nanosecondes par itération.

Des améliorations immédiates mineures

Vous comparez tous les éléments de la liste avec tous les autres, mais deux fois: d'abord vous comparer a dans la boucle extérieure avec b dans la boucle intérieure, puis plus tard, vous comparez b dans la boucle extérieure avec dans la boucle intérieure a.

La mesure Jaro-Winkler est symétrique , il est donc pas nécessaire de faire la comparaison deux fois. Dans la boucle interne, il suffit d'effectuer une itération dans les éléments restants. Cette amélioration est pas extraordinaire: il est toujours O (n ^ 2), mais ce sera au moins réduire votre temps d'exécution en deux.

Comme une remarque de côté, même si elle sera une amélioration significative par rapport à la question principale, en fonction de la langue, vous pourriez avoir quelques problèmes de performances liés aux listes (voir par exemple ici pour C # listes avec foreach , ou envisager l'allocation de mémoire/frais généraux de désaffectation pour les grandes listes en C++).

Une mémoire en Soluton de?

Si vous souhaitez juste besoin de trouver la dupe exacte, d'une manière beaucoup plus rapide serait:

  • Calculer un code de hachage lorsque vous parcourez à travers les lignes, et remplir une carte qui concerne le code de hachage à une liste de lignes de correspondance.
  • Après la première passe, vous pouvez itérer puis à travers la carte, ignorer les entrées avec une liste d'un seul élément, et traiter les listes qui ont plusieurs éléments (groupes potentiels à identifier, car le même hachage ne garantit pas toujours que c'est la même valeur).

La complexité de cet algorithme serait O(2n) donc 1 millions d'itérations dans le meilleur des cas, et O (n ^ 2) dans le pire des cas (hypothétique étant toutes les lignes sont une seule dupe ). le cas réel dépend du nombre de groupes en double et le nombre d'éléments dans chacun de ces groupes, mais je pense que ce soit un ordre de grandeur plus rapide que votre approche.

Le match floue pourrait être facilement mis en œuvre de la même manière, si la mise en correspondance peut être exprimé par une fonction de " normalisation " f() définie de telle sorte que f(record1)==f(record2) signifie qu'il ya un match. Cela fonctionnerait par exemple, si le match flou serait fondé sur des variantes du soundex

Malheureusement, le distance Jaro Winkler ne répond pas à cette exigence, de sorte que chaque ligne doit être comparé à tous les autres.

Une solution de base de données

Intuitivement, je dirais que l'utilisation d'une approche SGBD pourrait aussi faire le travail, surtout si votre correspondance floue est un peu plus complexe, et si vous travaillez avec des champs.

Peuplant un SGBD avec un demi-million de lignes devrait en principe prendre bien au-dessous de 24 heures si votre serveur est correctement dimensionné (transfert groupé en un seul passage). Énuméré SELECT ou une clause GROUP BY trouverait facilement les dupes exactes. La même chose est vrai pour une correspondance floue ayant une fonction de " normalisation ".

Mais les correspondances floues qui exigent une comparaison explicite, comme le Jaro-Winkler, il ne va pas aider beaucoup.

Divide et impera variante

Si votre métrique est pas appliquée sur la ligne dans son ensemble, mais sur un ensemble de champs, le SGBD pourrait réduire le nombre de comparaisons en travaillant au niveau du terrain. L'idée est d'éviter de comparer tous les enregistrements entre eux, mais considérer des sous-ensembles plus petits que pour lesquels l'effet de l'explosion combinatoire restent dans une fourchette raisonnable:

  • Pour le domaine concerné, sélectionnez les valeurs uniques. Ceux-ci forment souvent un plus petit ensemble.
  • Calculer la métrique dans ce petit ensemble, d'identifier les groupes potentiels
  • Ignorer les valeurs de paire avec une proximité insuffisante

Dans l'exemple suivant, vous souhaitez comparer seulement 3 valeurs au lieu de 5:

George
Melanie  
Georges 
George  
Melanie  

Ce qui aurait pour conséquence un seuil de 85% en:

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

Si plusieurs champs sont impliqués, vous souhaitez traiter individuellement chaque domaine afin d'identifier les potentiels prometteurs sous-groupes correspondants. Par exemple:

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

ajouterait une deuxième liste des groupes candidats après le retrait de la non prometteuses valeurs):

NEY-YORK / NEW IORK
OKLAHOMAI / OKLAHOMA 

Vous souhaitez ensuite sélectionner les enregistrements ayant toutes les valeurs de chacun des champs pertinents dans les groupes prometteurs: ici {George, Georges} et {NEW-YORK, NEW IORK, OKLAHOMAI, OKLAHOMA}. Les seuls dossiers seraient retournés:

George    NEW-YORK
Georges   NEW IORK
George    OKLAHOMI

Il y a alors deux stratégies:

  • soit vous exécutez votre algorithme sur les enregistrements sélectionnés, si le sous-ensemble est réduit suffisamment.
  • ou vous accélérez la mise en correspondance en recherchant tous les enregistrements seulement à ceux correspondant à une valeur de sous-groupe potentiel (vous pouvez imaginer ce une sorte de marquage avec la tête du sous-groupe de chaque champ, au détriment de l'espace).

La seconde approche se traduirait par:

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

Vous avez alors votre groupe en sélectionnant avec GROUP BY sur les étiquettes, en tenant compte bien sûr que des groupes ayant plus de 1 enregistrement.

1
Christophe