【问题标题】:Slicing Pandas rows with string match slow用字符串匹配慢速切片 Pandas 行
【发布时间】:2017-12-26 12:48:07
【问题描述】:

我基本上想学习一种更快的方法来使用基于正则表达式的条件切片来切片 Pandas 数据帧。例如下面的 df(string_column 有 4 个以上的变体,它们仅用于说明目的):

index, string_col1, string_col2, value
0, 'apple', 'this', 10
1, 'pen', 'is', 123
2, 'pineapple', 'sparta', 20
3, 'pen pineapple apple pen', 'this', 234
4, 'apple', 'is', 212
5, 'pen', 'sparta', 50
6, 'pineapple', 'this', 69
7, 'pen pineapple apple pen', 'is',  79
8, 'apple pen', 'sparta again', 78
...
100000, 'pen pineapple apple pen', 'this is sparta', 392

我必须使用正则表达式根据 string_column 进行布尔条件切片,同时在 value 列中找到具有最小值和最大值的索引,然后最终找到最小值和最大值之间的差异。我通过以下方法执行此操作,但是当我必须匹配许多不同的正则表达式模式时,它非常慢:

pat1 = re.compile('apple')
pat2 = re.compile('sparta')
mask = (df['string_col1'].str.contains(pat1)) & (df['string_col2'].str.contains(pat2))
max_idx = df[mask].idxmax()
min_idx = df[mask].idxmin()
difference = df['value'].loc[max_idx] - df['value'].loc[min_idx]

我认为要获得一个“差异”答案,我将 df 切片太多次,但我不知道如何减少它。此外,有没有更快的切片方法?

这是一个优化问题,因为我知道我的代码可以满足我的需求。任何提示将不胜感激!

【问题讨论】:

  • 可以将正则表达式组合成一个正则表达式,然后掩码可能会更快。 patX=re.compile('(apple|sprata)')。这会让它更快吗?此外,在整个 DataFrame 上制作掩码以获取第一个索引可能不是最快的。
  • 我需要对两个不同列的两种不同模式进行两次单独的正则表达式检查,所以我不确定将它们组合成一个正则表达式并在两列之间进行匹配是个好主意。
  • 您想要字符串或值列的 idxmax 吗?您的代码在 python3 中为我崩溃,但如果我正在阅读您的代码,正确的 idxmax 只是采用“字母顺序”最大值。这真的是你想要的吗?示例输出在这里也可能很好。
  • 为什么在这些检查中使用正则表达式?好像你可以使用df['string_col1'].str.contains('apple', case=False),不是吗?似乎它比编译正则表达式模式并使用它们要快。

标签: python pandas numpy optimization


【解决方案1】:

我一直在尝试分析您的示例,但实际上我在合成数据上的表现非常出色,因此我可能需要澄清一下。 (另外,由于某种原因,每当我的数据框中有一个字符串时,.idxmax() 都会中断)。

这是我的测试代码:

import pandas as pd
import re
import numpy as np
import random
import IPython
from timeit import default_timer as timer

possibilities_col1 = ['apple', 'pen', 'pineapple', 'joseph', 'cauliflower']
possibilities_col2 = ['sparta', 'this', 'is', 'again']
entries = 100000
potential_words_col1 = 4
potential_words_col2 = 3
def create_function_col1():
    result = []
    for x in range(random.randint(1, potential_words_col1)):
        result.append(random.choice(possibilities_col1))
    return " ".join(result)

def create_function_col2():
    result = []
    for x in range(random.randint(1, potential_words_col2)):
        result.append(random.choice(possibilities_col2))
    return " ".join(result)

data = {'string_col1': pd.Series([create_function_col1() for _ in range(entries)]),
        'string_col2': pd.Series([create_function_col2() for _ in range(entries)]),
        'value': pd.Series([random.randint(1, 500) for _ in range(entries)])}


df = pd.DataFrame(data)
pat1 = re.compile('apple')
pat2 = re.compile('sparta')
pat3 = re.compile('pineapple')
pat4 = re.compile('this')
#IPython.embed()
start = timer()
mask = df['string_col1'].str.contains(pat1) & \
       df['string_col1'].str.contains(pat3) & \
       df['string_col2'].str.contains(pat2) & \
       df['string_col2'].str.contains(pat4)
valid = df[mask]
max_idx = valid['value'].argmax()
min_idx = valid['value'].argmin()
#max_idx = result['max']
#min_idx = result['min']
difference = df.loc[max_idx, 'value'] - df.loc[min_idx, 'value']
end = timer()
print("Difference: {}".format(difference))
print("# Valid: {}".format(len(valid)))
print("Time Elapsed: {}".format(end-start))

你能解释一下你申请了多少个条件吗? (我添加的每个正则表达式只会增加大致线性的时间增加(即 2->3 正则表达式意味着运行时间增加 1.5 倍))。我还对条目数和两个潜在的字符串长度(potential_words 变量)进行了线性缩放。

作为参考,这段代码在我的机器上评估大约需要 0.15 秒(100 万个条目需要大约 1.5 秒)。

编辑:我是个白痴,没有做和你一样的事情(我取的是数据集中最小和最大索引处的值之间的差异,而不是最小和最大值之间的差异),但是修复它并没有真正增加运行时间。

编辑 2:idxmax() 如何知道在您的示例代码中选择最大值的列?

【讨论】:

    【解决方案2】:

    将每个掩码传递给数据帧的下一个子集,每个新过滤都发生在原始数据帧的较小子集上:

    pat1 = re.compile('apple')
    pat2 = re.compile('sparta')
    mask1 = df['string_col1'].str.contains(pat1)
    mask = (df[mask1]['string_col2'].str.contains(pat2))
    df1=df[mask1][mask]
    max_idx = df1['value'].idxmax()
    min_idx = df1['value'].idxmin()
    a,b=df1['value'].loc[max_idx],df1['value'].loc[min_idx]
    

    【讨论】:

    • 你能解释一下为什么会更快吗?
    • 因为每个新过滤都发生在原始数据帧的较小子集上
    • 这是一个理智的想法,但如果所有模式都匹配,则无济于事
    【解决方案3】:

    您可以通过不使用 & 而是使用 scipy.logical_and() 来将逻辑比较速度提高 50 倍

    a = pd.Series(sp.rand(10000) > 0.5)
    b = pd.Series(sp.rand(10000) > 0.5)
    
    %timeit sp.logical_and(a.values,b.values)
    100000 loops, best of 3: 6.31 µs per loop
    
    %timeit a & b
    1000 loops, best of 3: 390 µs per loop
    

    【讨论】:

    • 这不是 scipy.logical_and() 的函数,而是使用 .values()。使用 a.values 和 b.values 重试 %timeit,您将获得相同的时间。
    • 哇哦。意外!感谢您指出了这一点。关于程序的问题,我现在应该删除这个答案吗?
    【解决方案4】:

    我认为使用掩码缩小数据帧,然后在较小的帧上执行一组更简洁的操作会很有帮助。查找索引只是为了将其用作查找是不必要的 - 只需直接找到最大/最小:

    pat1 = re.compile('apple')
    pat2 = re.compile('sparta')
    mask = (df['string_col1'].str.contains(pat1)) & (df['string_col2'].str.contains(pat2))
    
    result = df.loc[mask, 'value']
    difference = result.max() - result.min()
    

    【讨论】:

    • 你在哪里缩小df,我没看到?
    • OP 正在为他执行的每个操作重新过滤他的整个 DF。我执行一次过滤器,然后对较小的结果集进行操作。见result = df.loc[mask, 'value']
    • 这是便宜的操作,试试测量吧
    • 您是否因为尝试相同的优化但使用链式索引而不是 .loc(参见文档:pandas.pydata.org/pandas-docs/stable/…)和更复杂的代码而对我投了反对票?您为您的回答提供了与我相同的理由:“因为每个新过滤都发生在原始数据帧的较小子集上。”
    猜你喜欢
    • 2014-11-29
    • 2017-11-20
    • 2020-06-18
    • 2016-06-09
    • 1970-01-01
    • 2020-11-27
    • 2017-12-12
    • 2017-01-01
    • 2017-07-15
    相关资源
    最近更新 更多