代码之家  ›  专栏  ›  技术社区  ›  Martlark

Winkler的python性能改进请求

  •  4
  • Martlark  · 技术社区  · 14 年前

    我是一个python n00b,我想对如何改进算法以提高该方法计算两个名字的jaro-winkler距离的性能提出一些建议。

    def winklerCompareP(str1, str2):
    """Return approximate string comparator measure (between 0.0 and 1.0)
    
    USAGE:
      score = winkler(str1, str2)
    
    ARGUMENTS:
      str1  The first string
      str2  The second string
    
    DESCRIPTION:
      As described in 'An Application of the Fellegi-Sunter Model of
      Record Linkage to the 1990 U.S. Decennial Census' by William E. Winkler
      and Yves Thibaudeau.
    
      Based on the 'jaro' string comparator, but modifies it according to whether
      the first few characters are the same or not.
    """
    
    # Quick check if the strings are the same - - - - - - - - - - - - - - - - - -
    #
    jaro_winkler_marker_char = chr(1)
    if (str1 == str2):
        return 1.0
    
    len1 = len(str1)
    len2 = len(str2)
    halflen = max(len1,len2) / 2 - 1
    
    ass1  = ''  # Characters assigned in str1
    ass2  = '' # Characters assigned in str2
    #ass1 = ''
    #ass2 = ''
    workstr1 = str1
    workstr2 = str2
    
    common1 = 0    # Number of common characters
    common2 = 0
    
    #print "'len1', str1[i], start, end, index, ass1, workstr2, common1"
    # Analyse the first string    - - - - - - - - - - - - - - - - - - - - - - - - -
    #
    for i in range(len1):
        start = max(0,i-halflen)
        end   = min(i+halflen+1,len2)
        index = workstr2.find(str1[i],start,end)
        #print 'len1', str1[i], start, end, index, ass1, workstr2, common1
        if (index > -1):    # Found common character
            common1 += 1
            #ass1 += str1[i]
            ass1 = ass1 + str1[i]
            workstr2 = workstr2[:index]+jaro_winkler_marker_char+workstr2[index+1:]
    #print "str1 analyse result", ass1, common1
    
    #print "str1 analyse result", ass1, common1
    # Analyse the second string - - - - - - - - - - - - - - - - - - - - - - - - -
    #
    for i in range(len2):
        start = max(0,i-halflen)
        end   = min(i+halflen+1,len1)
        index = workstr1.find(str2[i],start,end)
        #print 'len2', str2[i], start, end, index, ass1, workstr1, common2
        if (index > -1):    # Found common character
            common2 += 1
            #ass2 += str2[i]
            ass2 = ass2 + str2[i]
            workstr1 = workstr1[:index]+jaro_winkler_marker_char+workstr1[index+1:]
    
    if (common1 != common2):
        print('Winkler: Wrong common values for strings "%s" and "%s"' % \
                    (str1, str2) + ', common1: %i, common2: %i' % (common1, common2) + \
                    ', common should be the same.')
        common1 = float(common1+common2) / 2.0    ##### This is just a fix #####
    
    if (common1 == 0):
        return 0.0
    
    # Compute number of transpositions    - - - - - - - - - - - - - - - - - - - - -
    #
    transposition = 0
    for i in range(len(ass1)):
        if (ass1[i] != ass2[i]):
            transposition += 1
    transposition = transposition / 2.0
    
    # Now compute how many characters are common at beginning - - - - - - - - - -
    #
    minlen = min(len1,len2)
    for same in range(minlen+1):
        if (str1[:same] != str2[:same]):
            break
    same -= 1
    if (same > 4):
        same = 4
    
    common1 = float(common1)
    w = 1./3.*(common1 / float(len1) + common1 / float(len2) + (common1-transposition) / common1)
    
    wn = w + same*0.1 * (1.0 - w)
    return wn
    

    实例输出

    ZIMMERMANN  ARMIENTO    0.814583333
    ZIMMERMANN  ZIMMERMANN  1
    ZIMMERMANN  CANNONS         0.766666667
    CANNONS AKKER           0.8
    CANNONS ALDERSON    0.845833333
    CANNONS ALLANBY         0.833333333
    
    3 回复  |  直到 14 年前
        1
  •  4
  •   Justin Peel    14 年前

    我更关注的是优化以从python中获得更多信息,而不是优化算法,因为我认为这里没有多少算法上的改进。下面是我提出的一些Python优化。

    (1)。因为您似乎在使用python 2.x,所以将all range()更改为x range()的.range()会在迭代之前生成完整的数字列表,而x range会根据需要生成它们。

    (2)。对max和min进行以下替换:

    start = max(0,i-halflen)
    

    具有

    start = i - halflen if i > halflen else 0
    

    end = min(i+halflen+1,len2)
    

    具有

    end = i+halflen+1 if i+halflen+1 < len2 else len2
    

    第一个循环和第二个循环的相似循环。还有另一个min()在后面,还有一个max()在函数的开头,所以对它们也做同样的操作。替换min()和max()真的有助于减少时间。这些函数很方便,但比我用的方法要贵。

    (3)。使用common1而不是len(ass1)。您已经跟踪了common1中ass1的长度,所以让我们使用它,而不是调用一个昂贵的函数来再次找到它。

    (4)。更换以下代码:

    minlen = min(len1,len2)
    for same in xrange(minlen+1):
        if (str1[:same] != str2[:same]):
            break
    same -= 1
    

    具有

    for same in xrange(minlen):
        if str1[same] != str2[same]:
            break
    

    这主要是因为str1[:same]每次通过循环都创建一个新字符串,您将检查已经检查过的部分。此外,不需要检查 '' != '' 减量 same 如果我们不需要的话。

    (5)。使用 psyco 是一个即时编译器。下载并安装后,只需添加行

    import psyco
    psyco.full()
    

    在文件的顶部使用它。不要使用psyco,除非你做了我提到的其他更改。出于某种原因,当我在您的原始代码上运行它时,它实际上减慢了速度。

    使用TimeIt,我发现随着前4个变化,我的时间减少了大约20%。但是,当我将psyco与这些更改一起添加时,代码比原始代码快3到4倍。

    如果你想要更快的速度

    字符串的find()方法中有相当多的剩余时间。我决定用我自己的来代替这个。对于第一个循环,我替换了

    index = workstr2.find(str1[i],start,end)
    

    具有

    index = -1
    for j in xrange(start,end):
        if workstr2[j] == str1[i]:
            index = j
            break
    

    以及第二个循环的类似形式。没有psyco,这会减慢代码的速度,但是有了psyco,它会大大加快代码的速度。最后一次修改后,代码比原来的快8到9倍。

    如果速度不够快

    然后你可能会转向制作一个C模块。

    祝你好运!

        2
  •  2
  •   chmullig    14 年前

    我想如果你使用Pylevenshtein模块,你会做得更好。对于大多数用例来说,它是C并且相当快。它包括一个JaroWinkler函数,可以提供相同的输出,但在我的机器上,它的速度快63倍。

    In [1]: import jw
    
    In [2]: jw.winklerCompareP('ZIMMERMANN', 'CANNONS')
    Out[2]: 0.41428571428571426
    
    In [3]: timeit jw.winklerCompareP('ZIMMERMANN', 'CANNONS')
    10000 loops, best of 3: 28.2 us per loop
    
    In [4]: import Levenshtein
    
    In [5]: Levenshtein.jaro_winkler('ZIMMERMANN', 'CANNONS')
    Out[5]: 0.41428571428571431
    
    In [6]: timeit Levenshtein.jaro_winkler('ZIMMERMANN', 'CANNONS')
    1000000 loops, best of 3: 442 ns per loop
    
        3
  •  0
  •   Dave Kirby    14 年前

    除了贾斯汀所说的所有内容之外,串接字符串的开销也很大——python必须为新字符串分配内存,然后将两个字符串都复制到新字符串中。

    所以这是坏的:

    ass1 = ''
    for i in range(len1):
         ...
        if (index > -1):    # Found common character
            ...
            ass1 = ass1 + str1[i]
    

    创建ass1和ass2字符列表和使用 ass1.append(str1[i]) . 从我对代码的快速阅读中可以看出,您在后面对ass1和ass2所做的唯一一件事就是逐字符地迭代它们,这样它们就不需要是字符串。如果以后确实需要将它们用作字符串,则可以使用 ''.join(ass1) .