pandas中的for循环真的不好吗?我什么时候应该在意?

发布于 2021-02-02 23:21:47

for循环真正的“坏”?如果不是,在什么情况下它们会比使用更常规的“矢量化”方法更好?1个

我熟悉“矢量化”的概念,以及熊猫如何利用矢量化技术来加快计算速度。向量化功能在整个系列或DataFrame上广播操作,以实现比传统上迭代数据快得多的加速。

但是,我很惊讶地看到很多代码(包括来自Stack Overflow的答案)提供了解决问题的解决方案,这些问题涉及使用for循环和列表推导遍历数据。文档和API指出,循环是“不好的”循环,并且“绝不能”循环遍历数组,序列或DataFrame。那么,为什么有时我会看到用户建议基于循环的解决方案?

关注者
0
被浏览
217
1 个回答
  • 面试哥
    面试哥 2021-02-02
    为面试而生,有面试问题,就找面试哥。

    TLDR;不,for循环并非总会“坏”,至少并非总是如此。说某些矢量化操作比迭代慢,而不是说迭代快于某些矢量化操作,可能更准确。知道何时以及为什么是使代码获得最大性能的关键。简而言之,在这些情况下,值得考虑使用矢量化熊猫函数的替代方法:

    1. 当你的数据很小(…取决于你的工作)时,
    2. 处理object/ mixed dtypes
    3. 使用str/ regex访问器功能时

    让我们分别检查这些情况。

    小数据上的迭代v / s矢量化

    熊猫在其API设计中遵循“配置惯例”方法。这意味着已经安装了相同的API,以适应广泛的数据和用例。

    调用pandas函数时,该函数必须在内部处理以下各项(除其他事项外),以确保工作正常

    索引/轴对齐
    处理混合数据类型
    处理丢失的数据
    几乎每个函数都必须在不同程度上处理这些问题,这带来了开销。数字函数(例如Series.add)的开销较少,而字符串函数(例如Series.str.replace)的开销更为明显。


    for另一方面,循环比你想象的要快。更好的是列表理解(通过for循环创建列表)更快,因为它们是优化的列表创建迭代机制。

    列表理解遵循模式

    [f(x) for x in seq]
    

    seq熊猫系列或DataFrame列在哪里。或者,当对多列进行操作时,

    [f(x, y) for x, y in zip(seq1, seq2)]
    

    seq1seq2列。

    数值比较

    考虑一个简单的布尔索引操作。列表推导方法已针对Series.ne(!=)和进行计时query。功能如下:

    # Boolean indexing with Numeric value comparison.
    df[df.A != df.B]                            # vectorized !=
    df.query('A != B')                          # query (numexpr)
    df[[x != y for x, y in zip(df.A, df.B)]]    # list comp
    

    为简单起见,本文中使用了该perfplot包来运行所有的timeit测试。以上操作的时间安排如下:

    在此处输入图片说明

    query对于中等大小的N,列表理解要胜过,甚至对于较小的N而言,列表理解要胜过向量化不等于比较。不幸的是,列表理解是线性扩展的,因此对于较大的N而言,它不能提供太多的性能提升。

    注意
    值得一提的是,列表理解的许多好处来自于不必担心索引对齐,但是这意味着,如果你的代码依赖于索引对齐,则此操作会中断。在某些情况下,可以将对基础NumPy数组的矢量化操作视为“两全其美”,从而实现了矢量化,而没有熊猫函数的所有不必要开销。这意味着你可以将上面的操作重写为

    df[df.A.values != df.B.values]
    

    它的性能优于熊猫和列表理解同等物:

    NumPy矢量化不在本文讨论范围之内,但是如果性能很重要,则绝对值得考虑。

    值计数

    再举一个例子-这次是另一个比循环快的普通 python构造- collections.Counter。一个常见的要求是计算值计数并将结果作为字典返回。这与做value_counts,np.unique以及Counter:

    # Value Counts comparison.
    ser.value_counts(sort=False).to_dict()           # value_counts
    dict(zip(*np.unique(ser, return_counts=True)))   # np.unique
    Counter(ser)                                     # Counter
    

    结果更明显,Counter在较大的小N(〜3500)范围内胜过两种矢量化方法。

    注意
    更多琐事(由@ user2357112提供)。的Counter实现是使用C加速器实现的,因此尽管它仍然必须使用python对象而不是底层的C数据类型,但它仍比for循环快。Python的力量!

    当然,从这里获得的好处是性能取决于你的数据和用例。这些示例的目的是说服你不要将这些解决方案排除为合法选项。如果这些仍然不能满足你的需求,那么总会有cython和numba。让我们将此测试添加到混合中。

    from numba import njit, prange
    
    @njit(parallel=True)
    def get_mask(x, y):
        result = [False] * len(x)
        for i in prange(len(x)):
            result[i] = x[i] != y[i]
    
        return np.array(result)
    
    df[get_mask(df.A.values, df.B.values)] # numba
    

    Numba可以将循环python代码的JIT编译为功能非常强大的矢量化代码。了解如何使numba发挥作用需要学习。

    混合/ objectdtypes的操作

    基于字符串的比较再
    来看第一部分的过滤示例,如果要比较的列是字符串怎么办?考虑上面相同的3个函数,但将输入DataFrame强制转换为字符串。

    # Boolean indexing with string value comparison.
    df[df.A != df.B]                            # vectorized !=
    df.query('A != B')                          # query (numexpr)
    df[[x != y for x, y in zip(df.A, df.B)]]    # list comp
    

    那么,发生了什么变化?这里要注意的是,字符串操作本质上难以向量化。Pandas将字符串视为对象,并且对对象的所有操作都会回退到缓慢,循环的实现中。

    现在,由于此循环实现被上述所有开销所包围,因此,即使这些解决方案按比例缩放,它们之间也存在恒定的幅度差异。

    当涉及对可变/复杂对象的操作时,没有比较。列表理解胜过所有涉及字典和列表的操作。

    通过键访问字典值
    以下是从字典列中提取值的两个操作的时间安排:map和列表理解。该设置位于附录的“代码段”标题下。

    # Dictionary value extraction.
    ser.map(operator.itemgetter('value'))     # map
    pd.Series([x.get('value') for x in ser])  # list comprehension
    

    3个操作的位置列表索引计时,这些操作从列列表中提取第0个元素(处理异常)map,str.get访问器方法和列表理解:

    # List positional indexing. 
    def get_0th(lst):
        try:
            return lst[0]
        # Handle empty lists and NaNs gracefully.
        except (IndexError, TypeError):
            return np.nan
    ser.map(get_0th)                                          # map
    ser.str[0]                                                # str accessor
    pd.Series([x[0] if len(x) > 0 else np.nan for x in ser])  # list comp
    pd.Series([get_0th(x) for x in ser])                      # list comp safe
    

    注意
    如果索引很重要,则需要执行以下操作:

    pd.Series([...], index=ser.index)
    

    重建系列时。

    列表扁平

    化最后一个例子是扁平化列表。这是另一个常见问题,它演示了纯python的强大功能。

    # Nested list flattening.
    pd.DataFrame(ser.tolist()).stack().reset_index(drop=True)  # stack
    pd.Series(list(chain.from_iterable(ser.tolist())))         # itertools.chain
    pd.Series([y for x in ser for y in x])                     # nested list comp
    

    无论itertools.chain.from_iterable和嵌套列表理解是纯Python结构,并且规模比更好stack的解决方案。

    这些时间点充分说明了熊猫没有为混合dtypes做好准备的事实,并且你可能应该避免使用它来这样做。数据应尽可能在单独的列中作为标量值(整数/浮点数/字符串)存在。

    最后,这些解决方案的适用性在很大程度上取决于你的数据。因此,最好的办法是先决定对数据进行这些操作,然后再决定要做什么。请注意,我尚未apply对这些解决方案计时,因为它会使图形倾斜(是​​的,那太慢了)。

    正则表达式操作和访问器.str方法

    熊猫可以应用正则表达式的操作,如str.contains,str.extractstr.extractall,以及其他的“矢量”字符串操作(例如str.split,str.find ,str.translate,等等)的字符串列。这些功能比列表理解要慢,并且是比其他功能更方便的功能。

    预编译正则表达式模式并使用遍历数据通常要快得多re.compile(另请参阅使用Python的re.compile是否值得?)。列表组合等效于str.contains如下所示:

    p = re.compile(...)
    ser2 = pd.Series([x for x in ser if p.search(x)])
    

    要么,

    ser2 = ser[[bool(p.search(x)) for x in ser]]
    

    如果你需要处理NaN,则可以执行以下操作

    ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
    

    相当于str.extract(无组)的列表组合看起来像:

    df['col2'] = [p.search(x).group(0) for x in df['col']]
    

    如果你需要处理不匹配和NaN,则可以使用自定义函数(速度更快!):

    def matcher(x):
        m = p.search(str(x))
        if m:
            return m.group(0)
        return np.nan
    
    df['col2'] = [matcher(x) for x in df['col']]
    

    matcher功能是非常可扩展的。根据需要,它可以适合返回每个捕获组的列表。只需提取匹配对象的groupor groups属性的查询即可。

    对于str.extractall,更改p.searchp.findall

    字符串提取

    考虑简单的过滤操作。这个想法是提取一个大写字母开头的4位数字。

    # Extracting strings.
    p = re.compile(r'(?<=[A-Z])(\d{4})')
    def matcher(x):
        m = p.search(x)
        if m:
            return m.group(0)
        return np.nan
    
    ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False)   #  str.extract
    pd.Series([matcher(x) for x in ser])                  #  list comprehension
    


知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看