อัลกอริทึมการจัดอันดับความคล้ายคลึงกันที่ดีขึ้นสำหรับสตริงความยาวตัวแปร


152

ฉันกำลังมองหาอัลกอริทึมความคล้ายคลึงกันของสตริงที่ให้ผลลัพธ์ที่ดีกว่าบนสตริงความยาวผันแปรได้ดีกว่าที่แนะนำ (ระยะทาง levenshtein, soundex, ฯลฯ )

ตัวอย่างเช่น,

รับสตริง A: "Robert",

จากนั้นสตริง B: "Amy Robertson"

จะเป็นการแข่งขันที่ดีกว่า

สตริง C: "Richard"

นอกจากนี้ควรอัลกอริทึมนี้ควรเป็นผู้ไม่เชื่อเรื่องภาษา (ยังทำงานในภาษาอื่นที่ไม่ใช่ภาษาอังกฤษ)



คำตอบ:


155

Simon White of Catalysoft เขียนบทความเกี่ยวกับอัลกอริทึมที่ฉลาดมากที่เปรียบเทียบคู่อักขระที่อยู่ติดกันซึ่งทำงานได้ดีสำหรับวัตถุประสงค์ของฉัน:

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

Simon มีอัลกอริทึมรุ่น Java และด้านล่างฉันเขียนเวอร์ชัน PL / Ruby ของมัน (นำมาจากรุ่น ruby ​​ธรรมดาที่ทำในความคิดเห็นรายการฟอรัมที่เกี่ยวข้องโดย Mark Wong-VanHaren) เพื่อให้ฉันสามารถใช้ในแบบสอบถาม 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';

ทำงานเหมือนจับใจ!


32
คุณพบคำตอบและเขียนทั้งหมดนั้นใน 4 นาที? ที่น่าประทับใจ!
แมตต์ J

28
ฉันเตรียมคำตอบหลังจากการวิจัยและการใช้งาน ฉันวางมันไว้ที่นี่เพื่อผลประโยชน์ของใครก็ตามที่กำลังมองหา SO เพื่อหาคำตอบที่เป็นประโยชน์โดยใช้อัลกอริธึมทางเลือกเพราะคำตอบส่วนใหญ่ในคำถามที่เกี่ยวข้องดูเหมือนจะหมุนรอบ ๆ levenshtein หรือ soundex
marzagao

18
สิ่งที่ฉันกำลังมองหา คุณจะแต่งงานกับฉันไหม
BlackTea

6
@JasonSundram ถูกต้อง - อันที่จริงแล้วนี่เป็นสัมประสิทธิ์ลูกเต๋าที่รู้จักกันดีในกราฟขนาดใหญ่ระดับตัวอักษรเนื่องจากผู้เขียนเขียนใน "ภาคผนวก" (ด้านล่างของหน้า)
Fred Foo

4
สิ่งนี้จะคืนค่า "คะแนน" เป็น 1 (การจับคู่ 100%) เมื่อเปรียบเทียบสตริงที่มีตัวอักษรเดี่ยวโดดเดี่ยวเป็นความแตกต่างเช่นตัวอย่างนี้: string_similarity("vitamin B", "vitamin C") #=> 1มีวิธีที่ง่ายในการป้องกันพฤติกรรมแบบนี้หรือไม่?
MrYoshiji

77

คำตอบของ marzagaoนั้นยอดเยี่ยม ฉันแปลงเป็น C # ดังนั้นฉันคิดว่าฉันโพสต์ไว้ที่นี่:

ลิงค์ 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;
    }
}

2
+100 ถ้าทำได้คุณช่วยฉันทำงานหนักเป็นวัน ๆ ไชโย
vvohra87

1
ดีมาก! ข้อเสนอแนะเดียวที่ฉันมีคือจะทำให้มันเป็นส่วนขยาย
Levitikon

+1! ยอดเยี่ยมที่ใช้งานได้กับการปรับเปลี่ยนเล็กน้อยสำหรับ Java เช่นกัน และดูเหมือนว่าจะตอบสนองได้ดีกว่า Levenshtein
Xyene

1
ฉันเพิ่มเวอร์ชันที่แปลงเป็นวิธีส่วนขยายด้านล่าง ขอบคุณสำหรับรุ่นดั้งเดิมและการแปลที่ยอดเยี่ยม
Frank Rundatz

@Michael La Voie ขอบคุณมากมันเยี่ยมมาก! แม้ว่าปัญหาเล็กน้อยกับ(2.0 * intersection) / union- ฉันได้รับ Double.NaN เมื่อเปรียบเทียบสองสตริงว่าง
Vojtěch Dohnal

41

นี่คือคำตอบของ marzagaoอีกรุ่นหนึ่งอันนี้เขียนด้วย 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()

2
มีข้อผิดพลาดเล็ก ๆ น้อย ๆ ใน string_similarity เมื่อมีซ้ำ ngrams ในคำทำให้คะแนน> 1 สำหรับสตริงที่เหมือนกัน การเพิ่ม 'break' หลังจาก "hit_count + = 1" แก้ไขได้
jbaiter

1
@ jbaiter: จับได้ดี ฉันเปลี่ยนเพื่อให้สอดคล้องกับการเปลี่ยนแปลงของคุณ
John Rutledge

3
ในบทความของ Simon White เขากล่าวว่า "โปรดทราบว่าเมื่อใดก็ตามที่พบการจับคู่คู่อักขระนั้นจะถูกลบออกจากรายการอาร์เรย์ที่สองเพื่อป้องกันไม่ให้เราจับคู่กับอักขระคู่เดียวกันหลายครั้ง (มิเช่นนั้น 'GGGGG' กับ 'GG') "ฉันจะแก้ไขข้อความนี้เพื่อบอกว่ามันจะให้การจับคู่ที่สมบูรณ์แบบสูงกว่า โดยไม่คำนึงถึงสิ่งนี้ดูเหมือนว่าจะมีผลว่าอัลกอริทึมไม่ได้เป็นสกรรมกริยา (ความคล้ายคลึงกัน (x, y) = / = ความคล้ายคลึงกัน (y, x) การเพิ่ม pairs2.remove (y) หลังจากบรรทัด hit_count + = 1 แก้ไขปัญหาได้
NinjaMeTimbers

17

นี่คือการใช้งาน PHP ของอัลกอริทึม StrikeAMatch ที่แนะนำโดย Simon White ข้อดี (เหมือนที่บอกในลิงค์) คือ:

  • ภาพสะท้อนที่แท้จริงของความคล้ายคลึงกันของคำศัพท์ - สตริงที่มีความแตกต่างเล็กน้อยควรได้รับการยอมรับว่าคล้ายกัน โดยเฉพาะอย่างยิ่งการทับซับสตริงที่มีนัยสำคัญควรชี้ไปที่ระดับความคล้ายคลึงสูงระหว่างสตริง

  • ความทนทานต่อการเปลี่ยนแปลงลำดับของคำ - สองสายที่มีคำเดียวกัน แต่ในลำดับที่แตกต่างกันควรได้รับการยอมรับว่าคล้ายกัน ในทางกลับกันถ้าหนึ่งสตริงเป็นเพียงแอนนาแกรมสุ่มของตัวละครที่มีอยู่ในอื่น ๆ ก็ควรจะได้รับการยอมรับว่าเป็นที่แตกต่างกัน

  • ความเป็นอิสระทางภาษา - อัลกอริธึมไม่ควรทำงานเฉพาะในภาษาอังกฤษ แต่ในภาษาต่างๆ

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

17

คำตอบสั้น ๆ ของ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))

แม้แต่intersectionตัวแปรก็เป็นสายเสีย
Chibueze Opata

14

การอภิปรายนี้เป็นประโยชน์จริง ๆ ขอบคุณ ฉันแปลงอัลกอริธึมเป็น VBA สำหรับใช้กับ Excel และเขียนฟังก์ชันแผ่นงานสองสามรุ่นหนึ่งรายการสำหรับการเปรียบเทียบคู่สตริงอย่างง่ายอีกรายการหนึ่งสำหรับเปรียบเทียบสตริงหนึ่งเป็นช่วง / อาร์เรย์ของสตริง เวอร์ชัน strSimLookup ส่งคืนคู่ที่ดีที่สุดครั้งล่าสุดเป็นสตริงดัชนีอาร์เรย์หรือเมทริกความคล้ายคลึงกัน

การใช้งานนี้ให้ผลลัพธ์แบบเดียวกันกับที่ระบุไว้ในตัวอย่าง Amazon บนเว็บไซต์ของ Simon White โดยมีข้อยกเว้นเล็กน้อยบางประการเกี่ยวกับการจับคู่ที่ให้คะแนนต่ำ ไม่แน่ใจว่าความแตกต่างคืบคลานเข้ามาอาจเป็นฟังก์ชั่น Split ของ VBA แต่ฉันไม่ได้ตรวจสอบเพราะมันทำงานได้ดีสำหรับวัตถุประสงค์ของฉัน

'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 สิ่งนี้มีประโยชน์มาก แต่ฉันใหม่กับ VBA และมีความท้าทายเล็กน้อยจากรหัส เป็นไปได้หรือไม่ที่คุณจะโพสต์ไฟล์ Excel ซึ่งใช้ประโยชน์จากการบริจาคของคุณ? เพื่อจุดประสงค์ของฉันฉันหวังว่าจะใช้เพื่อจับคู่ชื่อที่คล้ายกันจากคอลัมน์เดียวใน Excel ที่มีรายการประมาณ 1,000 รายการ (ตัดตอนมาที่นี่: dropbox.com/s/ofdliln9zxgi882/first-names-excerpt.xlsx ) ฉันจะใช้การจับคู่เป็นคำพ้องในการค้นหาบุคคล (โปรดดูsoftwarerecs.stackexchange.com/questions/38227/ … )
bjornte

12

ฉันขอโทษคำตอบไม่ได้ถูกคิดค้นโดยผู้เขียน นี่เป็นอัลกอริทึมที่รู้จักกันดีซึ่งนำเสนอครั้งแรกโดย Digital Equipment Corporation และมักจะถูกเรียกว่า shingling

http://www.hpl.hp.com/techreports/Compaq-DEC/SRC-TN-1997-015.pdf


10

ฉันแปลอัลกอริทึมของ Simon White เป็น PL / pgSQL นี่คือผลงานของฉัน

<!-- 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'; 

ทำงานกับ PostgreSQL ของฉันที่ไม่มีการสนับสนุน plruby! ขอบคุณ!
hostnik

ขอบคุณ! คุณจะทำสิ่งนี้ใน Oracle SQL ได้อย่างไร
olovholm

พอร์ตนี้ไม่ถูกต้อง สตริงที่แน่นอนไม่ส่งคืน 1
Brandon Wigfield

9

String Similarity Metricsประกอบด้วยภาพรวมของเมทริกต่างๆที่ใช้ในการเปรียบเทียบสตริง ( Wikipediaมีภาพรวมด้วย) มากของตัวชี้วัดเหล่านี้จะดำเนินการในห้องสมุดsimmetrics

อีกตัวอย่างหนึ่งของการวัดซึ่งไม่รวมอยู่ในภาพรวมที่กำหนดนั้นเป็นตัวอย่างเช่นระยะการบีบอัด (พยายามประมาณความซับซ้อนของ Kolmogorov ) ซึ่งสามารถใช้สำหรับข้อความที่ยาวกว่าที่คุณนำเสนอเล็กน้อย

คุณอาจจะยังมองหาที่พิจารณาเรื่องที่กว้างมากของการประมวลผลภาษาธรรมชาติ แพ็คเกจ R เหล่านี้ช่วยให้คุณเริ่มต้นได้อย่างรวดเร็ว (หรืออย่างน้อยก็ให้แนวคิดบางอย่าง)

และอีกหนึ่งการแก้ไขล่าสุด - ค้นหาคำถามอื่น ๆ ในหัวข้อนี้ที่ SO มีคำถามที่เกี่ยวข้องค่อนข้างน้อย


9

อัลกอริทึมเวอร์ชั่น PHP ที่เร็วขึ้น:

/**
 *
 * @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;
}

สำหรับข้อมูลที่ฉันมี (ประมาณ 2,400 เปรียบเทียบ) ฉันมีเวลาทำงาน 0.58 วินาทีกับโซลูชันIgal Alkonเทียบกับ 0.35 วินาทีกับฉัน


9

รุ่นใน 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)

  }

6

นี่คือเวอร์ชั่น 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)
}

อัลกอริทึมนี้ดีกว่า แต่ค่อนข้างช้าสำหรับข้อมูลขนาดใหญ่ ฉันหมายความว่าหากเราต้องเปรียบเทียบ 10,000 คำกับอีก 15,000 คำมันช้าเกินไป เราสามารถเพิ่มประสิทธิภาพการแสดงในแง่ของความเร็วได้หรือไม่?
indra_patil

6

การโพสต์คำตอบของ marzagaoใน C99 ซึ่งได้แรงบันดาลใจจากอัลกอริธึมเหล่านี้

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

การทดสอบบางอย่างขึ้นอยู่กับบทความต้นฉบับ :

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

5

การสร้างเวอร์ชั่น C # ของ Michael La Voie ที่ยอดเยี่ยมตามคำขอเพื่อให้เป็นวิธีการขยายนี่คือสิ่งที่ฉันคิดขึ้นมา ประโยชน์หลักของการทำเช่นนี้คือคุณสามารถเรียงลำดับรายการทั่วไปตามการจับคู่เปอร์เซ็นต์ ตัวอย่างเช่นให้พิจารณาว่าคุณมีเขตข้อมูลสตริงชื่อ "เมือง" ในวัตถุของคุณ ผู้ใช้ค้นหาคำว่า "Chester" และคุณต้องการส่งคืนผลลัพธ์ตามลำดับจากมากไปหาน้อย ตัวอย่างเช่นคุณต้องการแสดงตัวอักษรของ Chester ให้ปรากฏก่อน Rochester เมื่อต้องการทำสิ่งนี้ให้เพิ่มคุณสมบัติใหม่สองรายการลงในวัตถุของคุณ:

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

จากนั้นในแต่ละวัตถุให้ตั้งค่า SearchText เป็นสิ่งที่ผู้ใช้ค้นหา จากนั้นคุณสามารถจัดเรียงได้อย่างง่ายดายด้วยสิ่งที่ชอบ:

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

นี่คือการปรับเปลี่ยนเล็กน้อยเพื่อให้เป็นวิธีการขยาย:

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

ฉันคิดว่าคุณน่าจะดีกว่าการใช้บูล isCaseSensitive ด้วยค่าเริ่มต้นของ false - แม้ว่าจะเป็นจริงการใช้งานนั้นสะอาดกว่ามาก
Jordan

5

การติดตั้ง JavaScript ของฉันใช้สตริงหรืออาร์เรย์ของสตริงและชั้นเสริม (ชั้นเริ่มต้นคือ 0.5) หากคุณผ่านสตริงมันจะส่งคืนจริงหรือเท็จขึ้นอยู่กับว่าคะแนนความคล้ายคลึงกันของสตริงนั้นมากกว่าหรือเท่ากับพื้น หากคุณผ่านอาเรย์ของสตริงมันจะส่งกลับอาเรย์ของสตริงที่มีคะแนนความคล้ายคลึงกันมากกว่าหรือเท่ากับพื้นโดยเรียงตามคะแนน

ตัวอย่าง:

'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"]

นี่มันคือ:

(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];
      });
    }
  };
})();

1
เครื่องดักฟัง / สะกดผิด! for(var j = 0; j < pairs1.length; j++){ควรเป็นfor(var j = 0; j < pairs2.length; j++){
เซิลร์

3

อัลกอริทึมสัมประสิทธิ์ลูกเต๋า (คำตอบของ Simon White / marzagao) ถูกนำมาใช้ใน Ruby ในวิธีการ pair_distance_similar ใน amatch gem

https://github.com/flori/amatch

อัญมณีนี้ยังมีการใช้งานของจำนวนของการจับคู่โดยประมาณและอัลกอริทึมการเปรียบเทียบสตริง: Levenshtein แก้ไขระยะทาง, ผู้ขายแก้ไขระยะทาง, ระยะทาง Hamming, ความยาวร่วมกันที่ยาวที่สุด, ความยาวย่อยทั่วไปที่ยาวที่สุด, ตัวชี้วัดระยะทางคู่ .


2

เวอร์ชัน Haskell อย่าลังเลที่จะแนะนำการแก้ไขเพราะฉันไม่ได้ทำ 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

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))

1

สิ่งที่เกี่ยวกับระยะทาง Levenshtein หารด้วยความยาวของสตริงแรก (หรือแบ่งความยาวต่ำสุด / สูงสุด / เฉลี่ยของฉันของสตริงทั้งสอง)? นั่นได้ผลสำหรับฉันจนถึงตอนนี้


อย่างไรก็ตามในการอ้างถึงโพสต์อื่นในหัวข้อนี้สิ่งที่ส่งคืนมักจะ "เอาแน่เอานอนไม่ได้" มันจัดอันดับ 'echo' คล้ายกับ 'dog'
Xyene

@Nox: ส่วน "หารด้วยความยาวของสตริงแรก" ของการตอบกลับนี้มีความสำคัญ นอกจากนี้สิ่งนี้ยังทำงานได้ดีกว่าอัลกอริธึมของ Dice ที่ได้รับการยกย่องอย่างมากสำหรับข้อผิดพลาดในการพิมพ์ผิดและการขนย้ายและแม้แต่การผันคำกริยาทั่วไป
รถกระบะโลแกน

1

เฮ้พวกฉันลองใช้จาวาสคริปต์แล้ว แต่ฉันยังใหม่กับมันไม่มีใครรู้วิธีที่จะทำเร็วกว่านี้เหรอ?

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

การใช้งานนี้ไม่ถูกต้อง ฟังก์ชั่น bigram หยุดพักเพื่อป้อนความยาว 0 วิธีการ string_similarity ไม่แบ่งในวงที่สองอย่างถูกต้องซึ่งอาจนำไปสู่การนับคู่หลายครั้งหลายครั้งนำไปสู่ค่าตอบแทนที่เกิน 100% และคุณก็ลืมที่จะประกาศxและyและคุณไม่ควรวนซ้ำผ่านลูปโดยใช้for..in..ลูป (ใช้for(..;..;..)แทน)
Rob W

1

นี่เป็นอีกเวอร์ชั่นของความคล้ายคลึงกันซึ่งอยู่ในดัชนีSørensen – Dice (คำตอบของ marzagao) บทความนี้เขียนใน 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);
}

1

ฉันกำลังมองหาการใช้ทับทิมบริสุทธิ์ของอัลกอริทึมที่ระบุโดยคำตอบของ @ marzagao น่าเสียดายที่ลิงค์ที่ระบุโดย @marzagao เสีย ในคำตอบ @ s01ipsist เขาระบุอัญมณีทับทิมamatchที่ดำเนินการไม่ได้อยู่ในทับทิมบริสุทธิ์ ดังนั้นผมจึง searchd เล็ก ๆ น้อย ๆ และพบอัญมณีfuzzy_matchซึ่งมีการดำเนินงานทับทิมบริสุทธิ์ (แม้ว่าการใช้อัญมณีนี้amatch) ที่นี่ ฉันหวังว่าสิ่งนี้จะช่วยคนอย่างฉัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.