用实例解析fuzz.ratio计算方法

转自:http://blog.sina.com.cn/s/blog_6aaea1760102x5d5.html

这两天做智能问答,也就是需要做语料库的匹配查找,用到了fuzz.ratio,但苦于不知道其中的运作原理,不敢确定如何使用。于是百度、谷歌,发现居然没有人对计算过程进行解析。于是只能通过源码查找相关函数进行逐步解析。为便于记忆,直接上实例说明:

 

1、首先该函数用法是

from fuzzywuzzy import fuzz

str1 = "我是小杜"

str2 = "你是小翟吗"

fuzz.ratio(str1,str2)  # 运行结果为:44

 

2、运行过程如下:

2.1 首先计算长度

>>>len(str1)  #12

>>>len(str2)  #15

>>>T = len(str1)+len(str2)  #27

2.2 然后提取计算匹配

from difflib import SequenceMatcher

m = SequenceMatcher(None, str1, str2)

print m.get_matching_blocks()

### 输出结果:[Match(a=3, b=3, size=6), Match(a=12, b=15, size=0)]

### 提取方法是一个一个做比较,比较出相同的一段,一段,记录每一段的相同开始、结束位置,和相同的长度。然后最后一段是结束符,长度为0

2.3 计算匹配数量

matches = reduce(lambda sum, triple: sum + triple[-1],m.get_matching_blocks(), 0)

print(matches)

### 输出结果: M = 6   所有size求和

2.4 计算匹配比例

>>>2*6/27    ## Ratio = 2*M/T = 0.4444

print m.ratio()

0.444444444444

2.5 转化成结果

>>>round(0.4444*100)  ## fuzz.ratio = 44

即运行结果:fuzz.ratio(str1,str2)  # 运行结果为:44

 

3、再来几个例子:

例2

str1 = "cxd1301 is a boy"
str2 = "zxr1304 is a girl"

## sumoflen = 33

## matches = 10

## 所以结果是 2*10/33*100取整为61

例3

str1 = "cxd1301 is a boy"
str2 = "a girl named zxr1304"

## sumoflen = 36

## matches = 4

## 所以结果是 2*4/36*100取整为22

例4

str1 = "cxd1301 is a boy"
str2 = "a boy is cxd1304"

## sumoflen = 32

## matches = 6

## 所以结果是 2*6/32*100取整为38

 

4、说明

这种匹配方法,对顺序是严格要求的,见例4.

 

5、源码的查看分析过程,如下:

1、首先该函数用法是

from fuzzywuzzy import fuzz

str1 = "我是小杜"
str2 = "你是小翟吗"
fuzz.ratio(str1,str2)  # 运行结果为:44
 
计算过程怎么实现?
 

2、想要知道这个fuzz.ratio是什么机制

进入fuzz.py比如直接ctrl+B,查看源码如下:

 

def ratio(s1, s2):
    s1, s2 = utils.make_type_consistent(s1, s2)  # 这个是对输入进行校验或者Unicode处理
    m = SequenceMatcher(None, s1, s2)            # 这个是匹配计算的核心算法了
    return utils.intr(100 * m.ratio())           # 输出是乘以100的结果,同时是转了字符串
 
3、从2知道,核心算法是SequenceMatcher,因此要进一步了解
从载入信息中找到源头:
from difflib import SequenceMatcher
 

4、在进入difflib中寻找SequenceMatcher这个类

里面的核心是str1,变成str2需要的步骤:替换、删除、插入、相等

# opcodes
#      a list of (tag, i1, i2, j1, j2) tuples, where tag is
#      one of
#          'replace'   a[i1:i2] should be replaced by b[j1:j2]
#          'delete'    a[i1:i2] should be deleted
#          'insert'    b[j1:j2] should be inserted
#          'equal'     a[i1:i2] == b[j1:j2]

 

5、然后关键的是我们调用的是m.ratio()

def ratio(self):
# 找到计算方法是匹配值输出结果为2.0*M/T,其中T是两个比较字符串的长度之和,而M是匹配的元素个数。取值范围明细是[0,1],完全一样是1,完全不同是0。
    """Return a measure of the sequences' similarity (float in [0,1]).
    Where T is the total number of elements in both sequences, and
    M is the number of matches, this is 2.0*M / T.
    Note that this is 1 if the sequences are identical, and 0 if
    they have nothing in common.

    .ratio() is expensive to compute if you haven't already computed
    .get_matching_blocks() or .get_opcodes(), in which case you may
    want to try .quick_ratio() or .real_quick_ratio() first to get an
    upper bound.

    >>> s = SequenceMatcher(None, "abcd", "bcde")
    >>> s.ratio()
    0.75
    >>> s.quick_ratio()
    0.75
    >>> s.real_quick_ratio()
    1.0
    """

#计算过程如下:
    matches = reduce(lambda sum, triple: sum + triple[-1],
                     self.get_matching_blocks(), 0)
    return _calculate_ratio(matches, len(self.a) + len(self.b))

其中:_calculate_ratio比较简单,就是2.0*M / T.

def _calculate_ratio(matches, length):
    if length:
        return 2.0 * matches / length
    return 1.0

 

比较麻烦的是:获取matches,用到get_matching_blocks()

def get_matching_blocks(self):
    """Return list of triples describing matching subsequences.
#寻找返回匹配子串,我们所需要的是这些所匹配的size的总和。

    Each triple is of the form (i, j, n), and means that
    a[i:i+n] == b[j:j+n].  The triples are monotonically increasing in i and in j
    The last triple is a dummy, (len(a), len(b), 0), and is the only triple with n==0.

    >>> s = SequenceMatcher(None, "abxcd", "abcd")
    >>> s.get_matching_blocks()
    [Match(a=0, b=0, size=2), Match(a=3, b=2, size=2), Match(a=5, b=4, size=0)]
    """

    if self.matching_blocks is not None:
        return self.matching_blocks
    la, lb = len(self.a), len(self.b)
    queue = [(0, la, 0, lb)]
    matching_blocks = []
    while queue:
        alo, ahi, blo, bhi = queue.pop()
        i, j, k = x = self.find_longest_match(alo, ahi, blo, bhi)
        
if k:   # if k is 0, there was no matching block
            
matching_blocks.append(x)
            if alo < i and blo < j:
                queue.append((alo, i, blo, j))
            if i+k < ahi and j+k < bhi:
                queue.append((i+k, ahi, j+k, bhi))
    matching_blocks.sort()

    
i1 = j1 = k1 = 0
    non_adjacent = []
    for i2, j2, k2 in matching_blocks:
        if i1 + k1 == i2 and j1 + k1 == j2:
            
k1 += k2
        else:
            
if k1:
                non_adjacent.append((i1, j1, k1))
            i1, j1, k1 = i2, j2, k2
    if k1:
        non_adjacent.append((i1, j1, k1))

    non_adjacent.append( (la, lb, 0) )
    self.matching_blocks = map(Match._make, non_adjacent)
    return self.matching_blocks
posted @ 2019-01-16 15:01  mxp_neu  阅读(1034)  评论(0)    收藏  举报