Un meilleur algorithme de classement de similarité pour les chaînes de longueur variable

152

Je recherche un algorithme de similarité de chaîne qui donne de meilleurs résultats sur des chaînes de longueur variable que ceux qui sont généralement suggérés (distance de levenshtein, soundex, etc.).

Par exemple,

Donnée la chaîne A: "Robert",

Puis chaîne B: "Amy Robertson"

serait un meilleur match que

Chaîne C: "Richard"

En outre, de préférence, cet algorithme doit être indépendant de la langue (fonctionne également dans des langues autres que l'anglais).

Marzagao
la source
similaire dans .net: stackoverflow.com/questions/83777/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Consultez également: Coefficient de dés
avid_useR

Réponses:

155

Simon White de Catalysoft a écrit un article sur un algorithme très intelligent qui compare des paires de caractères adjacentes qui fonctionne très bien pour mes besoins:

http://www.catalysoft.com/articles/StrikeAMatch.html

Simon a une version Java de l'algorithme et ci-dessous, j'en ai écrit une version PL / Ruby (tirée de la version ruby ​​simple faite dans le commentaire d'entrée du forum associé par Mark Wong-VanHaren) afin que je puisse l'utiliser dans mes requêtes PostgreSQL:

CREATE FUNCTION string_similarity(str1 varchar, str2 varchar)
RETURNS float8 AS '

str1.downcase! 
pairs1 = (0..str1.length-2).collect {|i| str1[i,2]}.reject {
  |pair| pair.include? " "}
str2.downcase! 
pairs2 = (0..str2.length-2).collect {|i| str2[i,2]}.reject {
  |pair| pair.include? " "}
union = pairs1.size + pairs2.size 
intersection = 0 
pairs1.each do |p1| 
  0.upto(pairs2.size-1) do |i| 
    if p1 == pairs2[i] 
      intersection += 1 
      pairs2.slice!(i) 
      break 
    end 
  end 
end 
(2.0 * intersection) / union

' LANGUAGE 'plruby';

Fonctionne comme un charme!

Marzagao
la source
32
Vous avez trouvé la réponse et écrit tout cela en 4 minutes? Impressionnant!
Matt J
28
J'ai préparé ma réponse après quelques recherches et mise en œuvre. Je le mets ici au profit de quiconque cherche dans SO une réponse pratique en utilisant un algorithme alternatif car la plupart des réponses aux questions connexes semblent tourner autour de levenshtein ou soundex.
marzagao
18
Juste ce que je cherchais. Veux-tu m'épouser?
BlackTea
6
@JasonSundram a raison - en fait, c'est le coefficient de Dice bien connu sur les bigrammes au niveau des personnages, comme l'écrit l'auteur dans «l'addendum» (en bas de la page).
Fred Foo le
4
Cela renvoie un "score" de 1 (100% match) lors de la comparaison de chaînes ayant une seule lettre isolée comme différence, comme cet exemple:, string_similarity("vitamin B", "vitamin C") #=> 1y a-t-il un moyen simple d'empêcher ce genre de comportement?
MrYoshiji
77

La réponse de marzagao est excellente. Je l'ai converti en C # alors j'ai pensé le poster ici:

Lien Pastebin

/// <summary>
/// This class implements string comparison algorithm
/// based on character pair similarity
/// Source: http://www.catalysoft.com/articles/StrikeAMatch.html
/// </summary>
public class SimilarityTool
{
    /// <summary>
    /// Compares the two strings based on letter pair matches
    /// </summary>
    /// <param name="str1"></param>
    /// <param name="str2"></param>
    /// <returns>The percentage match from 0.0 to 1.0 where 1.0 is 100%</returns>
    public double CompareStrings(string str1, string str2)
    {
        List<string> pairs1 = WordLetterPairs(str1.ToUpper());
        List<string> pairs2 = WordLetterPairs(str2.ToUpper());

        int intersection = 0;
        int union = pairs1.Count + pairs2.Count;

        for (int i = 0; i < pairs1.Count; i++)
        {
            for (int j = 0; j < pairs2.Count; j++)
            {
                if (pairs1[i] == pairs2[j])
                {
                    intersection++;
                    pairs2.RemoveAt(j);//Must remove the match to prevent "GGGG" from appearing to match "GG" with 100% success

                    break;
                }
            }
        }

        return (2.0 * intersection) / union;
    }

    /// <summary>
    /// Gets all letter pairs for each
    /// individual word in the string
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    private List<string> WordLetterPairs(string str)
    {
        List<string> AllPairs = new List<string>();

        // Tokenize the string and put the tokens/words into an array
        string[] Words = Regex.Split(str, @"\s");

        // For each word
        for (int w = 0; w < Words.Length; w++)
        {
            if (!string.IsNullOrEmpty(Words[w]))
            {
                // Find the pairs of characters
                String[] PairsInWord = LetterPairs(Words[w]);

                for (int p = 0; p < PairsInWord.Length; p++)
                {
                    AllPairs.Add(PairsInWord[p]);
                }
            }
        }

        return AllPairs;
    }

    /// <summary>
    /// Generates an array containing every 
    /// two consecutive letters in the input string
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    private string[] LetterPairs(string str)
    {
        int numPairs = str.Length - 1;

        string[] pairs = new string[numPairs];

        for (int i = 0; i < numPairs; i++)
        {
            pairs[i] = str.Substring(i, 2);
        }

        return pairs;
    }
}
Michael La Voie
la source
2
+100 si je le pouvais, vous venez de m'économiser une dure journée de travail! À votre santé.
vvohra87 du
1
Très agréable! La seule suggestion que j'ai, serait d'en faire une extension.
Levitikon
+1! Super que cela fonctionne, avec de légères modifications pour Java aussi. Et cela semble renvoyer de meilleures réponses que Levenshtein.
Xyene
1
J'ai ajouté une version convertissant cela en une méthode d'extension ci-dessous. Merci pour la version originale et la traduction géniale.
Frank Rundatz
@Michael La Voie Merci, c'est très sympa! Bien qu'un petit problème avec (2.0 * intersection) / union- j'obtiens Double.NaN en comparant deux chaînes vides.
Vojtěch Dohnal
41

Voici une autre version de la réponse de marzagao , celle-ci écrite en Python:

def get_bigrams(string):
    """
    Take a string and return a list of bigrams.
    """
    s = string.lower()
    return [s[i:i+2] for i in list(range(len(s) - 1))]

def string_similarity(str1, str2):
    """
    Perform bigram comparison between two strings
    and return a percentage match in decimal form.
    """
    pairs1 = get_bigrams(str1)
    pairs2 = get_bigrams(str2)
    union  = len(pairs1) + len(pairs2)
    hit_count = 0
    for x in pairs1:
        for y in pairs2:
            if x == y:
                hit_count += 1
                break
    return (2.0 * hit_count) / union

if __name__ == "__main__":
    """
    Run a test using the example taken from:
    http://www.catalysoft.com/articles/StrikeAMatch.html
    """
    w1 = 'Healed'
    words = ['Heard', 'Healthy', 'Help', 'Herded', 'Sealed', 'Sold']

    for w2 in words:
        print('Healed --- ' + w2)
        print(string_similarity(w1, w2))
        print()
John Rutledge
la source
2
Il y a un petit bogue dans string_similarity quand il y a des ngrams en double dans un mot, ce qui entraîne un score> 1 pour des chaînes identiques. L'ajout d'un "break" après "hit_count + = 1" le corrige.
jbaiter
1
@jbaiter: Bonne prise. Je l'ai changé pour refléter vos changements.
John Rutledge
3
Dans l'article de Simon White, il dit: "Notez que chaque fois qu'une correspondance est trouvée, cette paire de caractères est supprimée de la deuxième liste de tableaux pour nous empêcher de faire une correspondance avec la même paire de caractères plusieurs fois (sinon, 'GGGGG' obtiendrait une correspondance parfaite contre 'GG'.) "Je modifierais cette déclaration pour dire que cela donnerait une correspondance plus élevée que parfaite. Sans en tenir compte, il semble également avoir pour résultat que l'algorithme n'est pas transitif (similarité (x, y) = / = similarité (y, x)). L'ajout de paires2.remove (y) après la ligne hit_count + = 1 résout le problème.
NinjaMeTimbers
17

Voici mon implémentation PHP de l'algorithme StrikeAMatch suggéré, par Simon White. les avantages (comme indiqué dans le lien) sont:

  • Un vrai reflet de la similitude lexicale - les chaînes avec de petites différences doivent être reconnues comme étant similaires. En particulier, un chevauchement important de sous-chaînes doit indiquer un niveau élevé de similitude entre les chaînes.

  • Une robustesse aux changements d'ordre des mots - deux chaînes qui contiennent les mêmes mots, mais dans un ordre différent, doivent être reconnues comme étant similaires. D'un autre côté, si une chaîne n'est qu'un anagramme aléatoire des caractères contenus dans l'autre, alors elle devrait (généralement) être reconnue comme différente.

  • Indépendance linguistique - l'algorithme devrait fonctionner non seulement en anglais, mais dans de nombreuses langues différentes.

<?php
/**
 * LetterPairSimilarity algorithm implementation in PHP
 * @author Igal Alkon
 * @link http://www.catalysoft.com/articles/StrikeAMatch.html
 */
class LetterPairSimilarity
{
    /**
     * @param $str
     * @return mixed
     */
    private function wordLetterPairs($str)
    {
        $allPairs = array();

        // Tokenize the string and put the tokens/words into an array

        $words = explode(' ', $str);

        // For each word
        for ($w = 0; $w < count($words); $w++)
        {
            // Find the pairs of characters
            $pairsInWord = $this->letterPairs($words[$w]);

            for ($p = 0; $p < count($pairsInWord); $p++)
            {
                $allPairs[] = $pairsInWord[$p];
            }
        }

        return $allPairs;
    }

    /**
     * @param $str
     * @return array
     */
    private function letterPairs($str)
    {
        $numPairs = mb_strlen($str)-1;
        $pairs = array();

        for ($i = 0; $i < $numPairs; $i++)
        {
            $pairs[$i] = mb_substr($str,$i,2);
        }

        return $pairs;
    }

    /**
     * @param $str1
     * @param $str2
     * @return float
     */
    public function compareStrings($str1, $str2)
    {
        $pairs1 = $this->wordLetterPairs(strtoupper($str1));
        $pairs2 = $this->wordLetterPairs(strtoupper($str2));

        $intersection = 0;

        $union = count($pairs1) + count($pairs2);

        for ($i=0; $i < count($pairs1); $i++)
        {
            $pair1 = $pairs1[$i];

            $pairs2 = array_values($pairs2);
            for($j = 0; $j < count($pairs2); $j++)
            {
                $pair2 = $pairs2[$j];
                if ($pair1 === $pair2)
                {
                    $intersection++;
                    unset($pairs2[$j]);
                    break;
                }
            }
        }

        return (2.0*$intersection)/$union;
    }
}
Igal Alkon
la source
17

Une version plus courte de la réponse de John Rutledge :

def get_bigrams(string):
    '''
    Takes a string and returns a list of bigrams
    '''
    s = string.lower()
    return {s[i:i+2] for i in xrange(len(s) - 1)}

def string_similarity(str1, str2):
    '''
    Perform bigram comparison between two strings
    and return a percentage match in decimal form
    '''
    pairs1 = get_bigrams(str1)
    pairs2 = get_bigrams(str2)
    return (2.0 * len(pairs1 & pairs2)) / (len(pairs1) + len(pairs2))
quantum
la source
Même la intersectionvariable est un gaspillage de ligne.
Chibueze Opata
14

Cette discussion a été vraiment utile, merci. J'ai converti l'algorithme en VBA pour une utilisation avec Excel et j'ai écrit quelques versions d'une fonction de feuille de calcul, l'une pour une simple comparaison d'une paire de chaînes, l'autre pour comparer une chaîne à une plage / un tableau de chaînes. La version strSimLookup renvoie la dernière meilleure correspondance sous forme de chaîne, d'index de tableau ou de métrique de similarité.

Cette implémentation produit les mêmes résultats que ceux répertoriés dans l'exemple Amazon sur le site Web de Simon White, à quelques exceptions près sur les matchs à faible score; Je ne sais pas où la différence s'insinue, pourrait être la fonction Split de VBA, mais je n'ai pas enquêté car cela fonctionne bien pour mes besoins.

'Implements functions to rate how similar two strings are on
'a scale of 0.0 (completely dissimilar) to 1.0 (exactly similar)
'Source:   http://www.catalysoft.com/articles/StrikeAMatch.html
'Author: Bob Chatham, bob.chatham at gmail.com
'9/12/2010

Option Explicit

Public Function stringSimilarity(str1 As String, str2 As String) As Variant
'Simple version of the algorithm that computes the similiarity metric
'between two strings.
'NOTE: This verision is not efficient to use if you're comparing one string
'with a range of other values as it will needlessly calculate the pairs for the
'first string over an over again; use the array-optimized version for this case.

    Dim sPairs1 As Collection
    Dim sPairs2 As Collection

    Set sPairs1 = New Collection
    Set sPairs2 = New Collection

    WordLetterPairs str1, sPairs1
    WordLetterPairs str2, sPairs2

    stringSimilarity = SimilarityMetric(sPairs1, sPairs2)

    Set sPairs1 = Nothing
    Set sPairs2 = Nothing

End Function

Public Function strSimA(str1 As Variant, rRng As Range) As Variant
'Return an array of string similarity indexes for str1 vs every string in input range rRng
    Dim sPairs1 As Collection
    Dim sPairs2 As Collection
    Dim arrOut As Variant
    Dim l As Long, j As Long

    Set sPairs1 = New Collection

    WordLetterPairs CStr(str1), sPairs1

    l = rRng.Count
    ReDim arrOut(1 To l)
    For j = 1 To l
        Set sPairs2 = New Collection
        WordLetterPairs CStr(rRng(j)), sPairs2
        arrOut(j) = SimilarityMetric(sPairs1, sPairs2)
        Set sPairs2 = Nothing
    Next j

    strSimA = Application.Transpose(arrOut)

End Function

Public Function strSimLookup(str1 As Variant, rRng As Range, Optional returnType) As Variant
'Return either the best match or the index of the best match
'depending on returnTYype parameter) between str1 and strings in rRng)
' returnType = 0 or omitted: returns the best matching string
' returnType = 1           : returns the index of the best matching string
' returnType = 2           : returns the similarity metric

    Dim sPairs1 As Collection
    Dim sPairs2 As Collection
    Dim metric, bestMetric As Double
    Dim i, iBest As Long
    Const RETURN_STRING As Integer = 0
    Const RETURN_INDEX As Integer = 1
    Const RETURN_METRIC As Integer = 2

    If IsMissing(returnType) Then returnType = RETURN_STRING

    Set sPairs1 = New Collection

    WordLetterPairs CStr(str1), sPairs1

    bestMetric = -1
    iBest = -1

    For i = 1 To rRng.Count
        Set sPairs2 = New Collection
        WordLetterPairs CStr(rRng(i)), sPairs2
        metric = SimilarityMetric(sPairs1, sPairs2)
        If metric > bestMetric Then
            bestMetric = metric
            iBest = i
        End If
        Set sPairs2 = Nothing
    Next i

    If iBest = -1 Then
        strSimLookup = CVErr(xlErrValue)
        Exit Function
    End If

    Select Case returnType
    Case RETURN_STRING
        strSimLookup = CStr(rRng(iBest))
    Case RETURN_INDEX
        strSimLookup = iBest
    Case Else
        strSimLookup = bestMetric
    End Select

End Function

Public Function strSim(str1 As String, str2 As String) As Variant
    Dim ilen, iLen1, ilen2 As Integer

    iLen1 = Len(str1)
    ilen2 = Len(str2)

    If iLen1 >= ilen2 Then ilen = ilen2 Else ilen = iLen1

    strSim = stringSimilarity(Left(str1, ilen), Left(str2, ilen))

End Function

Sub WordLetterPairs(str As String, pairColl As Collection)
'Tokenize str into words, then add all letter pairs to pairColl

    Dim Words() As String
    Dim word, nPairs, pair As Integer

    Words = Split(str)

    If UBound(Words) < 0 Then
        Set pairColl = Nothing
        Exit Sub
    End If

    For word = 0 To UBound(Words)
        nPairs = Len(Words(word)) - 1
        If nPairs > 0 Then
            For pair = 1 To nPairs
                pairColl.Add Mid(Words(word), pair, 2)
            Next pair
        End If
    Next word

End Sub

Private Function SimilarityMetric(sPairs1 As Collection, sPairs2 As Collection) As Variant
'Helper function to calculate similarity metric given two collections of letter pairs.
'This function is designed to allow the pair collections to be set up separately as needed.
'NOTE: sPairs2 collection will be altered as pairs are removed; copy the collection
'if this is not the desired behavior.
'Also assumes that collections will be deallocated somewhere else

    Dim Intersect As Double
    Dim Union As Double
    Dim i, j As Long

    If sPairs1.Count = 0 Or sPairs2.Count = 0 Then
        SimilarityMetric = CVErr(xlErrNA)
        Exit Function
    End If

    Union = sPairs1.Count + sPairs2.Count
    Intersect = 0

    For i = 1 To sPairs1.Count
        For j = 1 To sPairs2.Count
            If StrComp(sPairs1(i), sPairs2(j)) = 0 Then
                Intersect = Intersect + 1
                sPairs2.Remove j
                Exit For
            End If
        Next j
    Next i

    SimilarityMetric = (2 * Intersect) / Union

End Function
Bchatham
la source
@bchatham Cela semble extrêmement utile, mais je suis nouveau sur VBA et un peu contesté par le code. Est-il possible pour vous de publier un fichier Excel qui utilise votre contribution? Pour mes besoins, j'espère l'utiliser pour faire correspondre des prénoms similaires d'une seule colonne dans Excel avec environ 1000 entrées (extrait ici: dropbox.com/s/ofdliln9zxgi882/first-names-excerpt.xlsx ). J'utiliserai ensuite les correspondances comme synonymes dans une recherche de personnes. (voir aussi softwarerecs.stackexchange.com/questions/38227/… )
bjornte
10

J'ai traduit l'algorithme de Simon White en PL / pgSQL. C'est ma contribution.

<!-- language: lang-sql -->

create or replace function spt1.letterpairs(in p_str varchar) 
returns varchar  as 
$$
declare

    v_numpairs integer := length(p_str)-1;
    v_pairs varchar[];

begin

    for i in 1 .. v_numpairs loop
        v_pairs[i] := substr(p_str, i, 2);
    end loop;

    return v_pairs;

end;
$$ language 'plpgsql';

--===================================================================

create or replace function spt1.wordletterpairs(in p_str varchar) 
returns varchar as
$$
declare
    v_allpairs varchar[];
    v_words varchar[];
    v_pairsinword varchar[];
begin
    v_words := regexp_split_to_array(p_str, '[[:space:]]');

    for i in 1 .. array_length(v_words, 1) loop
        v_pairsinword := spt1.letterpairs(v_words[i]);

        if v_pairsinword is not null then
            for j in 1 .. array_length(v_pairsinword, 1) loop
                v_allpairs := v_allpairs || v_pairsinword[j];
            end loop;
        end if;

    end loop;


    return v_allpairs;
end;
$$ language 'plpgsql';

--===================================================================

create or replace function spt1.arrayintersect(ANYARRAY, ANYARRAY)
returns anyarray as 
$$
    select array(select unnest($1) intersect select unnest($2))
$$ language 'sql';

--===================================================================

create or replace function spt1.comparestrings(in p_str1 varchar, in p_str2 varchar)
returns float as
$$
declare
    v_pairs1 varchar[];
    v_pairs2 varchar[];
    v_intersection integer;
    v_union integer;
begin
    v_pairs1 := wordletterpairs(upper(p_str1));
    v_pairs2 := wordletterpairs(upper(p_str2));
    v_union := array_length(v_pairs1, 1) + array_length(v_pairs2, 1); 

    v_intersection := array_length(arrayintersect(v_pairs1, v_pairs2), 1);

    return (2.0 * v_intersection / v_union);
end;
$$ language 'plpgsql'; 
fabiolimace
la source
Fonctionne sur mon PostgreSQL qui n'a pas de support plruby! Je vous remercie!
hostnik
Je vous remercie! Comment feriez-vous cela dans Oracle SQL?
olovholm
Ce port est incorrect. La chaîne exacte ne renvoie pas 1.
Brandon Wigfield
9

Les métriques de similarité de chaînes contiennent un aperçu de nombreuses métriques différentes utilisées dans la comparaison de chaînes ( Wikipedia a également une vue d'ensemble). Une grande partie de ces métriques est implémentée dans une bibliothèque simmetrics .

Encore un autre exemple de métrique, non inclus dans l'aperçu donné est par exemple la distance de compression (en essayant d'approcher la complexité de Kolmogorov ), qui peut être utilisée pour des textes un peu plus longs que celui que vous avez présenté.

Vous pourriez également envisager d'examiner un sujet beaucoup plus large du traitement du langage naturel . Ces packages R peuvent vous aider à démarrer rapidement (ou au moins vous donner des idées).

Et une dernière modification - recherchez les autres questions sur ce sujet à SO, il y en a pas mal de connexes.

Anonyme
la source
9

Une version PHP plus rapide de l'algorithme:

/**
 *
 * @param $str
 * @return mixed
 */
private static function wordLetterPairs ($str)
{
    $allPairs = array();

    // Tokenize the string and put the tokens/words into an array

    $words = explode(' ', $str);

    // For each word
    for ($w = 0; $w < count($words); $w ++) {
        // Find the pairs of characters
        $pairsInWord = self::letterPairs($words[$w]);

        for ($p = 0; $p < count($pairsInWord); $p ++) {
            $allPairs[$pairsInWord[$p]] = $pairsInWord[$p];
        }
    }

    return array_values($allPairs);
}

/**
 *
 * @param $str
 * @return array
 */
private static function letterPairs ($str)
{
    $numPairs = mb_strlen($str) - 1;
    $pairs = array();

    for ($i = 0; $i < $numPairs; $i ++) {
        $pairs[$i] = mb_substr($str, $i, 2);
    }

    return $pairs;
}

/**
 *
 * @param $str1
 * @param $str2
 * @return float
 */
public static function compareStrings ($str1, $str2)
{
    $pairs1 = self::wordLetterPairs(mb_strtolower($str1));
    $pairs2 = self::wordLetterPairs(mb_strtolower($str2));


    $union = count($pairs1) + count($pairs2);

    $intersection = count(array_intersect($pairs1, $pairs2));

    return (2.0 * $intersection) / $union;
}

Pour les données que j'avais (environ 2300 comparaisons), j'ai eu un temps de fonctionnement de 0,58sec avec la solution Igal Alkon contre 0,35sec avec le mien.

Andy
la source
9

Une version dans la belle Scala:

  def pairDistance(s1: String, s2: String): Double = {

    def strToPairs(s: String, acc: List[String]): List[String] = {
      if (s.size < 2) acc
      else strToPairs(s.drop(1),
        if (s.take(2).contains(" ")) acc else acc ::: List(s.take(2)))
    }

    val lst1 = strToPairs(s1.toUpperCase, List())
    val lst2 = strToPairs(s2.toUpperCase, List())

    (2.0 * lst2.intersect(lst1).size) / (lst1.size + lst2.size)

  }
Ignobilis
la source
6

Voici la version R:

get_bigrams <- function(str)
{
  lstr = tolower(str)
  bigramlst = list()
  for(i in 1:(nchar(str)-1))
  {
    bigramlst[[i]] = substr(str, i, i+1)
  }
  return(bigramlst)
}

str_similarity <- function(str1, str2)
{
   pairs1 = get_bigrams(str1)
   pairs2 = get_bigrams(str2)
   unionlen  = length(pairs1) + length(pairs2)
   hit_count = 0
   for(x in 1:length(pairs1)){
        for(y in 1:length(pairs2)){
            if (pairs1[[x]] == pairs2[[y]])
                hit_count = hit_count + 1
        }
   }
   return ((2.0 * hit_count) / unionlen)
}
Rainer
la source
Cet algorithme est meilleur mais assez lent pour les données volumineuses. Je veux dire que si l'on doit comparer 10000 mots avec 15000 autres mots, c'est trop lent. Pouvons-nous augmenter ses performances en termes de vitesse ??
indra_patil
6

Publier la réponse de Marzagao dans C99, inspirée de ces algorithmes

double dice_match(const char *string1, const char *string2) {

    //check fast cases
    if (((string1 != NULL) && (string1[0] == '\0')) || 
        ((string2 != NULL) && (string2[0] == '\0'))) {
        return 0;
    }
    if (string1 == string2) {
        return 1;
    }

    size_t strlen1 = strlen(string1);
    size_t strlen2 = strlen(string2);
    if (strlen1 < 2 || strlen2 < 2) {
        return 0;
    }

    size_t length1 = strlen1 - 1;
    size_t length2 = strlen2 - 1;

    double matches = 0;
    int i = 0, j = 0;

    //get bigrams and compare
    while (i < length1 && j < length2) {
        char a[3] = {string1[i], string1[i + 1], '\0'};
        char b[3] = {string2[j], string2[j + 1], '\0'};
        int cmp = strcmpi(a, b);
        if (cmp == 0) {
            matches += 2;
        }
        i++;
        j++;
    }

    return matches / (length1 + length2);
}

Quelques tests basés sur l' article original :

#include <stdio.h>

void article_test1() {
    char *string1 = "FRANCE";
    char *string2 = "FRENCH";
    printf("====%s====\n", __func__);
    printf("%2.f%% == 40%%\n", dice_match(string1, string2) * 100);
}


void article_test2() {
    printf("====%s====\n", __func__);
    char *string = "Healed";
    char *ss[] = {"Heard", "Healthy", "Help",
                  "Herded", "Sealed", "Sold"};
    int correct[] = {44, 55, 25, 40, 80, 0};
    for (int i = 0; i < 6; ++i) {
        printf("%2.f%% == %d%%\n", dice_match(string, ss[i]) * 100, correct[i]);
    }
}

void multicase_test() {
    char *string1 = "FRaNcE";
    char *string2 = "fREnCh";
    printf("====%s====\n", __func__);
    printf("%2.f%% == 40%%\n", dice_match(string1, string2) * 100);

}

void gg_test() {
    char *string1 = "GG";
    char *string2 = "GGGGG";
    printf("====%s====\n", __func__);
    printf("%2.f%% != 100%%\n", dice_match(string1, string2) * 100);
}


int main() {
    article_test1();
    article_test2();
    multicase_test();
    gg_test();

    return 0;
}
scripts
la source
5

En s'appuyant sur l'impressionnante version C # de Michael La Voie, conformément à la demande d'en faire une méthode d'extension, voici ce que j'ai trouvé. Le principal avantage de le faire de cette façon est que vous pouvez trier une liste générique en fonction du pourcentage de correspondance. Par exemple, considérez que vous avez un champ de chaîne nommé "Ville" dans votre objet. Un utilisateur recherche «Chester» et vous souhaitez renvoyer les résultats par ordre décroissant de correspondance. Par exemple, vous voulez que les correspondances littérales de Chester apparaissent avant Rochester. Pour ce faire, ajoutez deux nouvelles propriétés à votre objet:

    public string SearchText { get; set; }
    public double PercentMatch
    {
        get
        {
            return City.ToUpper().PercentMatchTo(this.SearchText.ToUpper());
        }
    }

Ensuite, sur chaque objet, définissez SearchText sur ce que l'utilisateur a recherché. Ensuite, vous pouvez le trier facilement avec quelque chose comme:

    zipcodes = zipcodes.OrderByDescending(x => x.PercentMatch);

Voici la légère modification pour en faire une méthode d'extension:

    /// <summary>
    /// This class implements string comparison algorithm
    /// based on character pair similarity
    /// Source: http://www.catalysoft.com/articles/StrikeAMatch.html
    /// </summary>
    public static double PercentMatchTo(this string str1, string str2)
    {
        List<string> pairs1 = WordLetterPairs(str1.ToUpper());
        List<string> pairs2 = WordLetterPairs(str2.ToUpper());

        int intersection = 0;
        int union = pairs1.Count + pairs2.Count;

        for (int i = 0; i < pairs1.Count; i++)
        {
            for (int j = 0; j < pairs2.Count; j++)
            {
                if (pairs1[i] == pairs2[j])
                {
                    intersection++;
                    pairs2.RemoveAt(j);//Must remove the match to prevent "GGGG" from appearing to match "GG" with 100% success

                    break;
                }
            }
        }

        return (2.0 * intersection) / union;
    }

    /// <summary>
    /// Gets all letter pairs for each
    /// individual word in the string
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    private static List<string> WordLetterPairs(string str)
    {
        List<string> AllPairs = new List<string>();

        // Tokenize the string and put the tokens/words into an array
        string[] Words = Regex.Split(str, @"\s");

        // For each word
        for (int w = 0; w < Words.Length; w++)
        {
            if (!string.IsNullOrEmpty(Words[w]))
            {
                // Find the pairs of characters
                String[] PairsInWord = LetterPairs(Words[w]);

                for (int p = 0; p < PairsInWord.Length; p++)
                {
                    AllPairs.Add(PairsInWord[p]);
                }
            }
        }

        return AllPairs;
    }

    /// <summary>
    /// Generates an array containing every 
    /// two consecutive letters in the input string
    /// </summary>
    /// <param name="str"></param>
    /// <returns></returns>
    private static  string[] LetterPairs(string str)
    {
        int numPairs = str.Length - 1;

        string[] pairs = new string[numPairs];

        for (int i = 0; i < numPairs; i++)
        {
            pairs[i] = str.Substring(i, 2);
        }

        return pairs;
    }
Frank Rundatz
la source
Je pense que vous feriez mieux d'utiliser un booléen isCaseSensitive avec une valeur par défaut de false - même si c'est vrai, l'implémentation est beaucoup plus propre
Jordanie
5

Mon implémentation JavaScript prend une chaîne ou un tableau de chaînes, et un plancher facultatif (le plancher par défaut est 0,5). Si vous lui passez une chaîne, elle retournera vrai ou faux selon que le score de similarité de la chaîne est supérieur ou égal au plancher. Si vous lui passez un tableau de chaînes, il renverra un tableau de ces chaînes dont le score de similarité est supérieur ou égal au plancher, triées par score.

Exemples:

'Healed'.fuzzy('Sealed');      // returns true
'Healed'.fuzzy('Help');        // returns false
'Healed'.fuzzy('Help', 0.25);  // returns true

'Healed'.fuzzy(['Sold', 'Herded', 'Heard', 'Help', 'Sealed', 'Healthy']);
// returns ["Sealed", "Healthy"]

'Healed'.fuzzy(['Sold', 'Herded', 'Heard', 'Help', 'Sealed', 'Healthy'], 0);
// returns ["Sealed", "Healthy", "Heard", "Herded", "Help", "Sold"]

C'est ici:

(function(){
  var default_floor = 0.5;

  function pairs(str){
    var pairs = []
      , length = str.length - 1
      , pair;
    str = str.toLowerCase();
    for(var i = 0; i < length; i++){
      pair = str.substr(i, 2);
      if(!/\s/.test(pair)){
        pairs.push(pair);
      }
    }
    return pairs;
  }

  function similarity(pairs1, pairs2){
    var union = pairs1.length + pairs2.length
      , hits = 0;

    for(var i = 0; i < pairs1.length; i++){
      for(var j = 0; j < pairs2.length; j++){
        if(pairs1[i] == pairs2[j]){
          pairs2.splice(j--, 1);
          hits++;
          break;
        }
      }
    }
    return 2*hits/union || 0;
  }

  String.prototype.fuzzy = function(strings, floor){
    var str1 = this
      , pairs1 = pairs(this);

    floor = typeof floor == 'number' ? floor : default_floor;

    if(typeof(strings) == 'string'){
      return str1.length > 1 && strings.length > 1 && similarity(pairs1, pairs(strings)) >= floor || str1.toLowerCase() == strings.toLowerCase();
    }else if(strings instanceof Array){
      var scores = {};

      strings.map(function(str2){
        scores[str2] = str1.length > 1 ? similarity(pairs1, pairs(str2)) : 1*(str1.toLowerCase() == str2.toLowerCase());
      });

      return strings.filter(function(str){
        return scores[str] >= floor;
      }).sort(function(a, b){
        return scores[b] - scores[a];
      });
    }
  };
})();
gruppler
la source
1
Bug / faute de frappe! for(var j = 0; j < pairs1.length; j++){devrait êtrefor(var j = 0; j < pairs2.length; j++){
Searle
3

L'algorithme de coefficient de Dice (réponse de Simon White / marzagao) est implémenté dans Ruby dans la méthode pair_distance_similar dans le gem amatch

https://github.com/flori/amatch

Ce joyau contient également des implémentations d'un certain nombre d'algorithmes d'appariement approximatif et de comparaison de chaînes: distance d'édition de Levenshtein, distance d'édition des vendeurs, distance de Hamming, la plus longue longueur de sous-séquence commune, la plus longue longueur de sous-chaîne commune, la métrique de distance de paire, la métrique Jaro-Winkler .

s01ipsist
la source
2

Une version Haskell - n'hésitez pas à suggérer des modifications car je n'ai pas beaucoup fait Haskell.

import Data.Char
import Data.List

-- Convert a string into words, then get the pairs of words from that phrase
wordLetterPairs :: String -> [String]
wordLetterPairs s1 = concat $ map pairs $ words s1

-- Converts a String into a list of letter pairs.
pairs :: String -> [String]
pairs [] = []
pairs (x:[]) = []
pairs (x:ys) = [x, head ys]:(pairs ys)

-- Calculates the match rating for two strings
matchRating :: String -> String -> Double
matchRating s1 s2 = (numberOfMatches * 2) / totalLength
  where pairsS1 = wordLetterPairs $ map toLower s1
        pairsS2 = wordLetterPairs $ map toLower s2
        numberOfMatches = fromIntegral $ length $ pairsS1 `intersect` pairsS2
        totalLength = fromIntegral $ length pairsS1 + length pairsS2
matthewpalmer
la source
2

Clojure:

(require '[clojure.set :refer [intersection]])

(defn bigrams [s]
  (->> (split s #"\s+")
       (mapcat #(partition 2 1 %))
       (set)))

(defn string-similarity [a b]
  (let [a-pairs (bigrams a)
        b-pairs (bigrams b)
        total-count (+ (count a-pairs) (count b-pairs))
        match-count (count (intersection a-pairs b-pairs))
        similarity (/ (* 2 match-count) total-count)]
    similarity))
Shaun Lebron
la source
1

Qu'en est-il de la distance de Levenshtein, divisée par la longueur de la première chaîne (ou encore divisée par la longueur min / max / moyenne des deux chaînes)? Cela a fonctionné pour moi jusqu'à présent.

Tehvan
la source
Cependant, pour citer un autre article sur ce sujet, ce qu'il renvoie est souvent "erratique". Il classe «écho» comme assez similaire à «chien».
Xyene
@Nox: La partie "divisé par la longueur de la première chaîne" de cette réponse est significative. En outre, cela fonctionne mieux que l'algorithme très apprécié de Dice pour les fautes de frappe et les erreurs de transposition, et même les conjugaisons courantes (pensez à comparer "nager" et "nager", par exemple).
Ramassage Logan
1

Hé les gars, j'ai essayé cela en javascript, mais je suis nouveau, quelqu'un connaît des moyens plus rapides de le faire?

function get_bigrams(string) {
    // Takes a string and returns a list of bigrams
    var s = string.toLowerCase();
    var v = new Array(s.length-1);
    for (i = 0; i< v.length; i++){
        v[i] =s.slice(i,i+2);
    }
    return v;
}

function string_similarity(str1, str2){
    /*
    Perform bigram comparison between two strings
    and return a percentage match in decimal form
    */
    var pairs1 = get_bigrams(str1);
    var pairs2 = get_bigrams(str2);
    var union = pairs1.length + pairs2.length;
    var hit_count = 0;
    for (x in pairs1){
        for (y in pairs2){
            if (pairs1[x] == pairs2[y]){
                hit_count++;
            }
        }
    }
    return ((2.0 * hit_count) / union);
}


var w1 = 'Healed';
var word =['Heard','Healthy','Help','Herded','Sealed','Sold']
for (w2 in word){
    console.log('Healed --- ' + word[w2])
    console.log(string_similarity(w1,word[w2]));
}
user779420
la source
Cette implémentation est incorrecte. La fonction bigramme est interrompue pour une entrée de longueur 0. La méthode string_similarity ne coupe pas correctement à l'intérieur de la deuxième boucle, ce qui peut conduire à compter les paires plusieurs fois, conduisant à une valeur de retour supérieure à 100%. Et vous avez également oublié de déclarer xet y, et vous ne devez pas parcourir les boucles en utilisant une for..in..boucle (utilisez for(..;..;..)plutôt).
Rob W
1

Voici une autre version de Similarity basée sur l'indice Sørensen – Dice (réponse de marzagao), celle-ci écrite en C ++ 11:

/*
 * Similarity based in Sørensen–Dice index.
 *
 * Returns the Similarity between _str1 and _str2.
 */
double similarity_sorensen_dice(const std::string& _str1, const std::string& _str2) {
    // Base case: if some string is empty.
    if (_str1.empty() || _str2.empty()) {
        return 1.0;
    }

    auto str1 = upper_string(_str1);
    auto str2 = upper_string(_str2);

    // Base case: if the strings are equals.
    if (str1 == str2) {
        return 0.0;
    }

    // Base case: if some string does not have bigrams.
    if (str1.size() < 2 || str2.size() < 2) {
        return 1.0;
    }

    // Extract bigrams from str1
    auto num_pairs1 = str1.size() - 1;
    std::unordered_set<std::string> str1_bigrams;
    str1_bigrams.reserve(num_pairs1);
    for (unsigned i = 0; i < num_pairs1; ++i) {
        str1_bigrams.insert(str1.substr(i, 2));
    }

    // Extract bigrams from str2
    auto num_pairs2 = str2.size() - 1;
    std::unordered_set<std::string> str2_bigrams;
    str2_bigrams.reserve(num_pairs2);
    for (unsigned int i = 0; i < num_pairs2; ++i) {
        str2_bigrams.insert(str2.substr(i, 2));
    }

    // Find the intersection between the two sets.
    int intersection = 0;
    if (str1_bigrams.size() < str2_bigrams.size()) {
        const auto it_e = str2_bigrams.end();
        for (const auto& bigram : str1_bigrams) {
            intersection += str2_bigrams.find(bigram) != it_e;
        }
    } else {
        const auto it_e = str1_bigrams.end();
        for (const auto& bigram : str2_bigrams) {
            intersection += str1_bigrams.find(bigram) != it_e;
        }
    }

    // Returns similarity coefficient.
    return (2.0 * intersection) / (num_pairs1 + num_pairs2);
}
chema989
la source
1

Je cherchais une implémentation pure ruby ​​de l'algorithme indiqué par la réponse de @ marzagao. Malheureusement, le lien indiqué par @marzagao est rompu. En réponse @ s01ipsist, il a indiqué bijou rubis amatch où la mise en œuvre n'est pas pur rubis. J'ai donc cherché un peu et trouvé gem fuzzy_match qui a une implémentation ruby ​​pure (bien que cette gemme l'utilise amatch) ici . J'espère que cela aidera quelqu'un comme moi.

Engr. Hasanuzzaman Sumon
la source