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?
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:
s
t
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.
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:
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:
ngram_record
table (ou collection)ngram_record
ngram
[.____]source
) [.____]N
ngams les plus pertinentsC
de source
longueurM
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 .
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.
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++).
Si vous souhaitez juste besoin de trouver la dupe exacte, d'une manière beaucoup plus rapide serait:
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.
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:
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:
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.