通过Pandas DataFrame搜索子字符串的最有效方法是什么?

时间:2018-08-14 14:09:26

标签: python string pandas dataframe series

我有一个Pandas数据框,其中包含75,000行文本(每行约350个字符)。我需要搜索该数据框中45k个子字符串列表的出现。

预期输出是authors_data dict,其中包含作者列表和出现次数。以下代码假定我有一个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()够快吗?我应该研究任何特定的库吗?

让我知道!

4 个答案:

答案 0 :(得分:2)

#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速度很慢。您可以改为将sum与生成器表达式和in运算符结合使用,以提高速度:

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 algorithm。有one example implementation,尽管对于每行中找到的 count 个元素可能需要做更多的工作。

设置

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']})

答案 1 :(得分:2)

我尝试了这个,它正在做您想要的。您可以测试一下,看看是否更快。

for author in authors_list:
            authors_data[author] = df['AUTHORCOL'].map(lambda x: author in x).sum()

答案 2 :(得分:1)

这不是一个完整的答案,但是您可以做一些事情来使事情变得更快:

-使用正则表达式:实际上是创建一个模式,然后编译Find out how many times a regex matches in a string in Python根据您的情况,每个作者只能编译一次。

-您有两个循环。假设作者数量合理,将最小的循环放入。您会惊讶于有时这有多么重要。这意味着,在移至下一行之前,搜索所有作者。 350个字符可以容纳到CPU的缓存中,如果运气好的话,可以节省很多时间。

将事情发挥到极限,但可能并不那么容易:编译模式是一种自动机,它仅查看输入的每个字符一次并识别输出(这就是您“编译”模式的原因 https://en.wikipedia.org/wiki/Deterministic_finite_automaton)。您可以创建所有自动机,然后从输入中获取每个字符并将其提供给所有自动机。然后,您将只“一次”处理每个输入字符(乘以作者的非恒定大小)

答案 3 :(得分:1)

单线可能会有所帮助。

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