winkler的Python性能改进请求

时间:2010-04-30 01:53:51

标签: python optimization performance jaro-winkler

我是一个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 个答案:

答案 0 :(得分:4)

我更专注于优化以获得更多的Python,而不是优化算法,因为我不认为这里有很多算法改进。以下是我提出的一些Python优化。

(1)。由于您似乎使用的是Python 2.x,因此将所有range()更改为xrange()。 range()在迭代之前生成完整的数字列表,而xrange根据需要生成它们。

(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模块。

祝你好运!

答案 1 :(得分:3)

我想如果使用PyLevenshtein模块,你可以做得更好。它是C并且对于大多数用例来说非常快。它包含一个jaro-winkler函数,可以提供相同的输出,但在我的机器上它的速度提高了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

答案 2 :(得分:0)

除了Justin所说的一切,连接字符串也很昂贵 - 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)转换它们。