将列表中的相似字符串组合在一起
Grouping similar strings together from a list
今天早上我一直在为办公室的一个问题而苦苦挣扎。
我需要找到一种方法将列表中的字符串组合在一起。很难解释,所以这里有一个例子:
假设我有一个列表如下:
['MONTREAL EDUCATION BOARD', 'Île de Montréal', 'Montréal',
'Ville de Montréal', 'MONTREAL CITY', 'Monrtéal', 'Mont-réal',
'Toronto', 'Toronto city', 'Tornoto', 'What is this', 'Bananasplit',
'Banana', 'StLouis', 'St-Louis', 'Saint Louis']
我需要找到一种方法,根据它们的相似性将这些值组合在一起:
[['MONTREAL EDUCATION BOARD'],
['Île de Montréal', 'Montréal','Ville de Montréal', 'MONTREAL CITY', 'Monrtéal', 'Mont-réal'],
['Toronto', 'Toronto city', 'Tornoto'],
['anything'],
['Bananasplit', 'Banana'],
['StLouis', 'St-Louis', 'Saint Louis']
]
那将是完美的案例。显然它可能有(并且将会)错误。我需要使用大约 10 000 个列表来执行此操作,每个列表包含 5 到 15 000 个字符串。我需要尽量减少错误并获得最好的分组。
我使用的是 fuzzywuzzy
的略微修改版本。我首先去掉重音并将所有字母大写以获得更准确的编辑距离。
我尝试的是设置一个阈值(假设为 80),遍历列表,将每个字符串组成一个组并删除重复的元素。显然这不是我需要的结果,因为我需要每个元素只出现在一个列表中(事实并非如此,因为 A 可以链接到 B,B 可以链接到 C,但不能链接到 A 到 C)。
groups = []
for curr in lst:
curr_grp = []
for item in lst:
ratio = normalized.partial_ratio(curr, item)
if ratio > SET_THRESHOLD:
curr_grp.append((item, ratio))
groups.append(curr_grp)
我认为可能有一种方法可以从我的输出中找到最佳配置:
[[('MONTREAL EDUCATION BOARD', 100),
('Montréal', 100), # Will probably have to use ratio() and not partial_ratio() because
('Monrtéal', 88), # this can't happen, EDUCATION BOARD is NOT Montreal
('Mont-réal', 89)],
[('Île de Montréal', 100),
('Montréal', 100),
('Ville de Montréal', 93),
('Monrtéal', 88),
('Mont-réal', 94)],
[('MONTREAL EDUCATION BOARD', 100),
('Île de Montréal', 100),
('Montréal', 100),
('Ville de Montréal', 100),
('MONTREAL CITY', 100),
('Monrtéal', 88),
('Mont-réal', 88)],
[('Île de Montréal', 93),
('Montréal', 100),
('Ville de Montréal', 100),
('Monrtéal', 88),
('Mont-réal', 94)],
[('Montréal', 100),
('MONTREAL CITY', 100),
('Monrtéal', 88),
('Mont-réal', 89)],
[('MONTREAL EDUCATION BOARD', 88),
('Île de Montréal', 88),
('Montréal', 88),
('Ville de Montréal', 88),
('MONTREAL CITY', 88),
('Monrtéal', 100)],
[('MONTREAL EDUCATION BOARD', 89),
('Île de Montréal', 94),
('Montréal', 88),
('Ville de Montréal', 94),
('MONTREAL CITY', 89),
('Mont-réal', 100)],
[('Toronto', 100), ('Toronto city', 100), ('Tornoto', 86)],
[('Toronto', 100), ('Toronto city', 100), ('Tornoto', 86)],
[('Toronto', 86), ('Toronto city', 86), ('Tornoto', 100)],
[('What is this', 100)],
[('Bananasplit', 100), ('Banana', 100)],
[('Bananasplit', 100), ('Banana', 100)],
[('StLouis', 100), ('St-Louis', 86), ('Saint Louis', 86)],
[('StLouis', 86), ('St-Louis', 100)],
[('StLouis', 86), ('Saint Louis', 100)]]
是否可以找到此列表中每个元素仅出现在一组中的最佳子集? (所以得分最高?)考虑到我的列表会更大,所以我无法测试每个配置,因为这需要数年时间。
否则,是否有另一种更有效的方法来完成我正在尝试做的事情?
谢谢!
您可以使用字典逐步分组,只包含尚未分组的城市。
请注意,我没有 fussywuzzy,所以我创建了一个贫民窟比率计算器来测试解决方案。我还删除了重音字符以使其更容易(我的 objective 不是为了创建一个好的字符串比较函数)
from collections import Counter
stripJunk = str.maketrans("","","- ")
def getRatio(a,b):
a = a.lower().translate(stripJunk)
b = b.lower().translate(stripJunk)
total = len(a)+len(b)
counts = (Counter(a)-Counter(b))+(Counter(b)-Counter(a))
return 100 - 100 * sum(counts.values()) / total
这是分组逻辑(您可以将我自定义的 getRatio() 函数替换为来自 fuzzywuzzy 的函数):
data = ['MONTREAL EDUCATION BOARD', 'Ile de Montreal', 'Montreal',
'Ville de Montreal', 'MONTREAL CITY', 'Monrteal', 'Mont-real',
'Toronto', 'Toronto city', 'Tornoto', 'What is this', 'Bananasplit',
'Banana', 'StLouis', 'St Louis', 'Saint Louis']
treshold = 75
minGroupSize = 1
from itertools import combinations
paired = { c:{c} for c in data }
for a,b in combinations(data,2):
if getRatio(a,b) < treshold: continue
paired[a].add(b)
paired[b].add(a)
groups = list()
ungrouped = set(data)
while ungrouped:
bestGroup = {}
for city in ungrouped:
g = paired[city] & ungrouped
for c in g.copy():
g &= paired[c]
if len(g) > len(bestGroup):
bestGroup = g
if len(bestGroup) < minGroupSize : break # to terminate grouping early change minGroupSize to 3
ungrouped -= bestGroup
groups.append(bestGroup)
groups
变量是一个包含城市名称集(组)的列表。城市只会出现在一组中。
# With a treshold of 75%:
{'MONTREAL CITY', 'Montreal', 'Monrteal', 'Mont-real'}
{'St Louis', 'StLouis', 'Saint Louis'}
{'Toronto', 'Toronto city', 'Tornoto'}
{'Ville de Montreal', 'Ile de Montreal'}
{'MONTREAL EDUCATION BOARD'}
{'Bananasplit'}
{'Banana'}
{'What is this'}
使用较低的阈值(或更好的比较功能),您将获得更少的组:
# With a treshold of 65%:
{'Monrteal', 'Montreal', 'Ville de Montreal', 'MONTREAL CITY', 'Mont-real', 'Ile de Montreal'}
{'Toronto', 'Toronto city', 'Tornoto'}
{'Saint Louis', 'StLouis', 'St Louis'}
{'Banana', 'Bananasplit'}
{'What is this'}
{'MONTREAL EDUCATION BOARD'}
从性能的角度来看,这将在合理的时间内为相对较小的数据集产生结果。将 1600 个城市分组需要 83 秒。由于 combinations() 循环的 O(N^2) 性质,当列表中的项目达到 15,000 时,这可能会变得不切实际。
分组循环从较大的组开始。它占用了大约一半的处理时间。一旦你达到足够小的群体,你就可以通过停止它来节省一些时间。也就是说,如果您不需要无数的 1-2 个城市群。当组大小变得小于 3 并且 1600 个城市在 48 秒内被处理时,我尝试停止分组循环(这对我的模拟数据来说是一个重要的节省)。不过,您可能无法通过实际数据获得如此大的性能提升。
今天早上我一直在为办公室的一个问题而苦苦挣扎。
我需要找到一种方法将列表中的字符串组合在一起。很难解释,所以这里有一个例子:
假设我有一个列表如下:
['MONTREAL EDUCATION BOARD', 'Île de Montréal', 'Montréal',
'Ville de Montréal', 'MONTREAL CITY', 'Monrtéal', 'Mont-réal',
'Toronto', 'Toronto city', 'Tornoto', 'What is this', 'Bananasplit',
'Banana', 'StLouis', 'St-Louis', 'Saint Louis']
我需要找到一种方法,根据它们的相似性将这些值组合在一起:
[['MONTREAL EDUCATION BOARD'],
['Île de Montréal', 'Montréal','Ville de Montréal', 'MONTREAL CITY', 'Monrtéal', 'Mont-réal'],
['Toronto', 'Toronto city', 'Tornoto'],
['anything'],
['Bananasplit', 'Banana'],
['StLouis', 'St-Louis', 'Saint Louis']
]
那将是完美的案例。显然它可能有(并且将会)错误。我需要使用大约 10 000 个列表来执行此操作,每个列表包含 5 到 15 000 个字符串。我需要尽量减少错误并获得最好的分组。
我使用的是 fuzzywuzzy
的略微修改版本。我首先去掉重音并将所有字母大写以获得更准确的编辑距离。
我尝试的是设置一个阈值(假设为 80),遍历列表,将每个字符串组成一个组并删除重复的元素。显然这不是我需要的结果,因为我需要每个元素只出现在一个列表中(事实并非如此,因为 A 可以链接到 B,B 可以链接到 C,但不能链接到 A 到 C)。
groups = []
for curr in lst:
curr_grp = []
for item in lst:
ratio = normalized.partial_ratio(curr, item)
if ratio > SET_THRESHOLD:
curr_grp.append((item, ratio))
groups.append(curr_grp)
我认为可能有一种方法可以从我的输出中找到最佳配置:
[[('MONTREAL EDUCATION BOARD', 100),
('Montréal', 100), # Will probably have to use ratio() and not partial_ratio() because
('Monrtéal', 88), # this can't happen, EDUCATION BOARD is NOT Montreal
('Mont-réal', 89)],
[('Île de Montréal', 100),
('Montréal', 100),
('Ville de Montréal', 93),
('Monrtéal', 88),
('Mont-réal', 94)],
[('MONTREAL EDUCATION BOARD', 100),
('Île de Montréal', 100),
('Montréal', 100),
('Ville de Montréal', 100),
('MONTREAL CITY', 100),
('Monrtéal', 88),
('Mont-réal', 88)],
[('Île de Montréal', 93),
('Montréal', 100),
('Ville de Montréal', 100),
('Monrtéal', 88),
('Mont-réal', 94)],
[('Montréal', 100),
('MONTREAL CITY', 100),
('Monrtéal', 88),
('Mont-réal', 89)],
[('MONTREAL EDUCATION BOARD', 88),
('Île de Montréal', 88),
('Montréal', 88),
('Ville de Montréal', 88),
('MONTREAL CITY', 88),
('Monrtéal', 100)],
[('MONTREAL EDUCATION BOARD', 89),
('Île de Montréal', 94),
('Montréal', 88),
('Ville de Montréal', 94),
('MONTREAL CITY', 89),
('Mont-réal', 100)],
[('Toronto', 100), ('Toronto city', 100), ('Tornoto', 86)],
[('Toronto', 100), ('Toronto city', 100), ('Tornoto', 86)],
[('Toronto', 86), ('Toronto city', 86), ('Tornoto', 100)],
[('What is this', 100)],
[('Bananasplit', 100), ('Banana', 100)],
[('Bananasplit', 100), ('Banana', 100)],
[('StLouis', 100), ('St-Louis', 86), ('Saint Louis', 86)],
[('StLouis', 86), ('St-Louis', 100)],
[('StLouis', 86), ('Saint Louis', 100)]]
是否可以找到此列表中每个元素仅出现在一组中的最佳子集? (所以得分最高?)考虑到我的列表会更大,所以我无法测试每个配置,因为这需要数年时间。
否则,是否有另一种更有效的方法来完成我正在尝试做的事情?
谢谢!
您可以使用字典逐步分组,只包含尚未分组的城市。
请注意,我没有 fussywuzzy,所以我创建了一个贫民窟比率计算器来测试解决方案。我还删除了重音字符以使其更容易(我的 objective 不是为了创建一个好的字符串比较函数)
from collections import Counter
stripJunk = str.maketrans("","","- ")
def getRatio(a,b):
a = a.lower().translate(stripJunk)
b = b.lower().translate(stripJunk)
total = len(a)+len(b)
counts = (Counter(a)-Counter(b))+(Counter(b)-Counter(a))
return 100 - 100 * sum(counts.values()) / total
这是分组逻辑(您可以将我自定义的 getRatio() 函数替换为来自 fuzzywuzzy 的函数):
data = ['MONTREAL EDUCATION BOARD', 'Ile de Montreal', 'Montreal',
'Ville de Montreal', 'MONTREAL CITY', 'Monrteal', 'Mont-real',
'Toronto', 'Toronto city', 'Tornoto', 'What is this', 'Bananasplit',
'Banana', 'StLouis', 'St Louis', 'Saint Louis']
treshold = 75
minGroupSize = 1
from itertools import combinations
paired = { c:{c} for c in data }
for a,b in combinations(data,2):
if getRatio(a,b) < treshold: continue
paired[a].add(b)
paired[b].add(a)
groups = list()
ungrouped = set(data)
while ungrouped:
bestGroup = {}
for city in ungrouped:
g = paired[city] & ungrouped
for c in g.copy():
g &= paired[c]
if len(g) > len(bestGroup):
bestGroup = g
if len(bestGroup) < minGroupSize : break # to terminate grouping early change minGroupSize to 3
ungrouped -= bestGroup
groups.append(bestGroup)
groups
变量是一个包含城市名称集(组)的列表。城市只会出现在一组中。
# With a treshold of 75%:
{'MONTREAL CITY', 'Montreal', 'Monrteal', 'Mont-real'}
{'St Louis', 'StLouis', 'Saint Louis'}
{'Toronto', 'Toronto city', 'Tornoto'}
{'Ville de Montreal', 'Ile de Montreal'}
{'MONTREAL EDUCATION BOARD'}
{'Bananasplit'}
{'Banana'}
{'What is this'}
使用较低的阈值(或更好的比较功能),您将获得更少的组:
# With a treshold of 65%:
{'Monrteal', 'Montreal', 'Ville de Montreal', 'MONTREAL CITY', 'Mont-real', 'Ile de Montreal'}
{'Toronto', 'Toronto city', 'Tornoto'}
{'Saint Louis', 'StLouis', 'St Louis'}
{'Banana', 'Bananasplit'}
{'What is this'}
{'MONTREAL EDUCATION BOARD'}
从性能的角度来看,这将在合理的时间内为相对较小的数据集产生结果。将 1600 个城市分组需要 83 秒。由于 combinations() 循环的 O(N^2) 性质,当列表中的项目达到 15,000 时,这可能会变得不切实际。
分组循环从较大的组开始。它占用了大约一半的处理时间。一旦你达到足够小的群体,你就可以通过停止它来节省一些时间。也就是说,如果您不需要无数的 1-2 个城市群。当组大小变得小于 3 并且 1600 个城市在 48 秒内被处理时,我尝试停止分组循环(这对我的模拟数据来说是一个重要的节省)。不过,您可能无法通过实际数据获得如此大的性能提升。