Python 熊猫的圈套真的很糟糕吗?我什么时候该在乎?

Python 熊猫的圈套真的很糟糕吗?我什么时候该在乎?,python,pandas,iteration,vectorization,list-comprehension,Python,Pandas,Iteration,Vectorization,List Comprehension,for循环的真的“坏”吗?如果不是,在什么情况下他们会比使用更传统的“矢量化”方法更好?1 我熟悉“矢量化”的概念,以及熊猫如何使用矢量化技术来加速计算。矢量化函数在整个系列或数据帧上广播操作,以实现比传统数据迭代更大的加速 然而,我非常惊讶地看到,许多代码(包括来自堆栈溢出的答案)提供了解决方案,这些问题涉及使用for循环和列表理解来循环数据。文档和API说循环是“坏的”,应该“永远”迭代数组、序列或数据帧。那么,为什么我有时会看到用户建议基于循环的解决方案呢 1-虽然这个问题听起来确实有点

for循环的
真的“坏”吗?如果不是,在什么情况下他们会比使用更传统的“矢量化”方法更好?1

我熟悉“矢量化”的概念,以及熊猫如何使用矢量化技术来加速计算。矢量化函数在整个系列或数据帧上广播操作,以实现比传统数据迭代更大的加速

然而,我非常惊讶地看到,许多代码(包括来自堆栈溢出的答案)提供了解决方案,这些问题涉及使用
for
循环和列表理解来循环数据。文档和API说循环是“坏的”,应该“永远”迭代数组、序列或数据帧。那么,为什么我有时会看到用户建议基于循环的解决方案呢



1-虽然这个问题听起来确实有点宽泛,但事实是在一些非常特殊的情况下,
for
循环通常比传统的数据迭代要好。这篇文章旨在为子孙后代捕捉这一点

TLDR;不,
for
循环不是“坏的”,至少,不总是这样。与说迭代比一些向量化操作快相比,说一些向量化操作比迭代慢可能更准确。了解何时以及为什么是使代码获得最佳性能的关键。简而言之,在这些情况下,值得考虑矢量化函数的替代方案:

# 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
  • 当您的数据很小时(取决于您正在做什么)
  • 处理
    对象时
    /混合数据类型
  • 使用
    str
    /regex访问器函数时
  • 让我们逐个检查这些情况


    小数据上的迭代v/s矢量化 Pandas在其API设计中采用了一种方法。这意味着已安装相同的API以满足广泛的数据和用例

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

  • 索引/轴对齐
  • 处理混合数据类型
  • 处理丢失的数据
  • 几乎每个函数都必须在不同程度上处理这些问题,这会带来开销。数值函数(例如,)的开销较小,而字符串函数(例如,)的开销更大

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

    列表理解遵循这种模式

    [f(x) for x in seq]
    
    其中
    seq
    是熊猫系列或数据帧列。或者,当在多个列上操作时

    [f(x, y) for x, y in zip(seq1, seq2)]
    
    其中
    seq1
    seq2
    是列

    数值比较
    考虑一个简单的布尔索引操作。列表理解方法已针对(
    !=
    )和进行计时。以下是功能:

    # 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
    
    为了简单起见,我在本文中使用了这个包来运行所有的timeit测试。上述操作的时间安排如下:

    对于中等大小的N,列表理解优于
    query
    ,对于微小的N,甚至优于向量化的不等于比较。不幸的是,列表理解是线性扩展的,因此对于较大的N,它不会提供太多性能增益

    注意
    值得一提的是,列表理解的许多好处来自于不必担心索引对齐, 但这意味着如果代码依赖于索引对齐, 这会打破的。在某些情况下,在 底层NumPy阵列可以被视为带来了“最好的 “两个世界”,允许矢量化,而无需所有不必要的功能开销。这意味着您可以将上述操作重写为

    df[df.A.values != df.B.values]
    
    它的表现优于pandas和list理解等价物:

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

    值计数
    再举一个例子——这次是另一个比for循环更快的普通python构造。一个常见的需求是计算值计数并将结果作为字典返回。这是通过和
    计数器
    完成的:

    # 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
    

    结果更加明显,
    计数器
    在更大范围的小N(~3500)中胜过两种矢量化方法

    注意
    更多琐事(由@user2357112提供)。
    计数器
    由, 因此,尽管它仍然必须使用python对象,而不是 在底层C数据类型中,它仍然比
    for
    循环快。python 力量

    当然,这里的好处是性能取决于您的数据和用例。这些示例的目的是说服您不要将这些解决方案排除在合法选项之外。如果这些仍然不能提供您所需的性能,那么总是会有。让我们把这个测试加入到混合物中

    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工作需要一个学习曲线


    混合/
    对象的操作
    d类型
    基于字符串的比较
    回顾第一节中的过滤示例,如果要比较的列是字符串,该怎么办?考虑上面的3个函数,但是将输入数据框转换为String。
    # 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
    

    那么,发生了什么变化?这里需要注意的是字符串操作本质上很难实现向量化
    # 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)
    
    # 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
    
    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]]
    
    ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
    
    df['col2'] = [p.search(x).group(0) for x in df['col']]
    
    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']]
    
    # 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
    
    import perfplot  
    import operator 
    import pandas as pd
    import numpy as np
    import re
    
    from collections import Counter
    from itertools import chain
    
    # Boolean indexing with Numeric value comparison.
    perfplot.show(
        setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B']),
        kernels=[
            lambda df: df[df.A != df.B],
            lambda df: df.query('A != B'),
            lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
            lambda df: df[get_mask(df.A.values, df.B.values)]
        ],
        labels=['vectorized !=', 'query (numexpr)', 'list comp', 'numba'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N'
    )
    
    # Value Counts comparison.
    perfplot.show(
        setup=lambda n: pd.Series(np.random.choice(1000, n)),
        kernels=[
            lambda ser: ser.value_counts(sort=False).to_dict(),
            lambda ser: dict(zip(*np.unique(ser, return_counts=True))),
            lambda ser: Counter(ser),
        ],
        labels=['value_counts', 'np.unique', 'Counter'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=lambda x, y: dict(x) == dict(y)
    )
    
    # Boolean indexing with string value comparison.
    perfplot.show(
        setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B'], dtype=str),
        kernels=[
            lambda df: df[df.A != df.B],
            lambda df: df.query('A != B'),
            lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
        ],
        labels=['vectorized !=', 'query (numexpr)', 'list comp'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )
    
    # Dictionary value extraction.
    ser1 = pd.Series([{'key': 'abc', 'value': 123}, {'key': 'xyz', 'value': 456}])
    perfplot.show(
        setup=lambda n: pd.concat([ser1] * n, ignore_index=True),
        kernels=[
            lambda ser: ser.map(operator.itemgetter('value')),
            lambda ser: pd.Series([x.get('value') for x in ser]),
        ],
        labels=['map', 'list comprehension'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )
    
    # List positional indexing. 
    ser2 = pd.Series([['a', 'b', 'c'], [1, 2], []])        
    perfplot.show(
        setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
        kernels=[
            lambda ser: ser.map(get_0th),
            lambda ser: ser.str[0],
            lambda ser: pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]),
            lambda ser: pd.Series([get_0th(x) for x in ser]),
        ],
        labels=['map', 'str accessor', 'list comprehension', 'list comp safe'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )
    
    # Nested list flattening.
    ser3 = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']])
    perfplot.show(
        setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
        kernels=[
            lambda ser: pd.DataFrame(ser.tolist()).stack().reset_index(drop=True),
            lambda ser: pd.Series(list(chain.from_iterable(ser.tolist()))),
            lambda ser: pd.Series([y for x in ser for y in x]),
        ],
        labels=['stack', 'itertools.chain', 'nested list comp'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',    
        equality_check=None
    
    )
    
    # Extracting strings.
    ser4 = pd.Series(['foo xyz', 'test A1234', 'D3345 xtz'])
    perfplot.show(
        setup=lambda n: pd.concat([ser4] * n, ignore_index=True),
        kernels=[
            lambda ser: ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False),
            lambda ser: pd.Series([matcher(x) for x in ser])
        ],
        labels=['str.extract', 'list comprehension'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )