如何在Pandas DataFrame中高效地搜索子字符串?

4

我有一个Pandas数据框,包含75000行文本(每行大约350个字符)。我需要在该数据框中搜索一个包含45000个子字符串的列表。

期望的输出是一个authors_data字典,包含作者列表和出现次数。下面的代码假定我有一个dataframe['text']列和一个名为authors_list的子字符串列表。

authors_data = {}
for author in authors_list:
    count = 0
    for i, row in df.iterrows():
         if author in row.text:
             count += 1
authors_data[author] = count
print(author, authors_data[author])

我进行了一些初始测试,10个作者花费我大约50秒的时间。完整的表格需要我运行几天的时间。因此,我正在寻找更加高效的运行代码的方法。

df.iterrows()足够快吗?我应该查看哪些特定的库?

请告诉我!


1
你能展示一下你的数据框的例子吗?几行就足够了,包括有多个作者的行。 - jpp
1
我认为这是一个典型的Cython转换机会,可以参考https://pandas.pydata.org/pandas-docs/stable/enhancingperf.html。此外,我相信使用iterrows()循环遍历是一种性能不佳的策略,最好使用numpy进行向量化或使用列表推导式。但你应该实现几种策略并自己测试时间。 - 3pitt
关于这个主题的一个非常有帮助的答案因某种原因被删除了。我会发布给我的那个答案,但是是的,列表推导式可以得到惊人的更好结果。我会阅读你的教程,听起来像是我需要的东西。 - sovnheim
4个回答

3

#1 分隔值

如果您的作者可以清楚地进行分隔,例如在每个系列元素中使用逗号分隔,那么您可以使用collections.Counteritertools.chain

from collections import Counter
from itertools import chain

res = Counter(chain.from_iterable(df['Authors'].str.split(',').map(set)))

# Counter({'Frank Herbert': 1, 'George Orwell': 2, 'John Steinbeck': 1,
#          'John Williams': 2, 'Philip K Dick': 1, 'Philip Roth': 1,
#          'Ursula K Le Guin': 1})

#2 任意字符串

当然,并非总是能获得这样的结构化数据。如果您的系列元素是带有任意数据的字符串,而您预定义的作者列表很小,则可以使用 pd.Series.str.contains

L = ['George Orwell', 'John Steinbeck', 'Frank Herbert', 'John Williams']

res = {i: df['Authors'].str.contains(i, regex=False).sum() for i in L}

# {'Frank Herbert': 1, 'George Orwell': 2, 'John Steinbeck': 1, 'John Williams': 2}

这是因为 pd.Series.str.contains 返回一个布尔值系列,您可以对其进行求和,因为在Python / Pandas的大多数数字计算中, True 被视为等同于 1 。我们关闭 regex 以提高性能。

性能

Pandas基于字符串的方法因其速度缓慢而闻名。您可以改用使用生成器表达式和 in 运算符的 sum 来额外加速:
df = pd.concat([df]*100000)

%timeit {i: df['Authors'].str.contains(i, regex=False).sum() for i in L}    # 420 ms
%timeit {i: sum(i in x for x in df['Authors'].values) for i in L}           # 235 ms
%timeit {i: df['Authors'].map(lambda x: i in x).sum() for i in L}           # 424 ms

请注意,对于方案#1,Counter方法实际上更加昂贵,因为它们需要将分割作为预备步骤:

chainer = chain.from_iterable

%timeit Counter(chainer([set(i.split(',')) for i in df['Authors'].values]))  # 650 ms
%timeit Counter(chainer(df['Authors'].str.split(',').map(set)))              # 828 ms

进一步改进

  1. 方案#2并不完美,因为它们不能区分(例如)John WilliamsJohn Williamson。如果这种区分对您很重要,您可能希望使用专业软件包。
  2. 对于#1和#2,您可能希望考虑使用Aho-Corasick算法。有一个示例实现,但是可能需要更多的工作来计算每行中找到的元素数量。

设置

df = pd.DataFrame({'Authors': ['Ursula K Le Guin,Philip K Dick,Frank Herbert,Ursula K Le Guin',
                               'John Williams,Philip Roth,John Williams,George Orwell',
                               'George Orwell,John Steinbeck,George Orwell,John Williams']})

我的作者列表非常清晰,所以这里不用担心。我只需遍历一组字符串列表即可。而且列表推导式比我之前使用的方法要快得多。非常感谢。 - sovnheim

3
我尝试过这个方法,它可以达到你想要的效果。你可以测试一下,看看是否更快。
for author in authors_list:
            authors_data[author] = df['AUTHORCOL'].map(lambda x: author in x).sum()

很高兴我能帮到你! :) - Olivier Turcotte

1

虽然不是完整的答案,但有一些事情可以让您加快速度:

-使用正则表达式:实际上创建一个模式,然后编译它。例如 在Python中查找字符串中正则表达式匹配的次数。在您的情况下,您可以每个作者只编译一次。

-您有两个循环。假设有合理数量的作者,将最小的循环放在内部。有时这会使您感到惊讶地重要。这意味着,在移动到下一行之前,为所有作者执行搜索。 350个字符可以适合CPU的缓存,如果您很幸运,可以节省大量时间。

将事情推向极限,但可能并不容易:编译后的模式是一个自动机,只查看输入的每个字符一次,并识别输出(这就是为什么要“编译”模式https://en.wikipedia.org/wiki/Deterministic_finite_automaton)。您可以创建所有自动机,然后将输入中的每个字符提取并馈送到所有自动机中。然后,您只需要“一次”处理每个输入字符(乘以作者数量的非常数大小)。

1

一行代码可能会很有帮助。

authors_data = {author: df.text.map(lambda x: author in x).sum() for author in authors_list}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接