对于与熊猫的循环-我何时应该关心?

时间:2019-01-03 18:54:21

标签: python pandas iteration vectorization list-comprehension

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

但是,我很惊讶地看到许多代码(包括来自Stack Overflow的答案)提供了解决问题的解决方案,这些问题涉及到使用for循环和列表解析来循环遍历数据。在阅读了文档并且对API有了一个不错的理解之后,我相信循环是“不好的”,并且应该“永远”不要遍历数组,序列或DataFrames。那么,我怎么会看到用户时不时地建议使用环路解决方案?

总而言之,我的问题是:
for循环真的“不好”吗?如果不是,在什么情况下它们会比使用更常规的“矢量化”方法更好? 1

1-尽管确实听起来有些疑问,但是事实是,在某些特定情况下,for循环通常比传统上遍历数据更好。这篇文章的目的是为了后代。

2 个答案:

答案 0 :(得分:83)

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

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

让我们逐一检查这些情况。


小数据迭代v / s矢量化

Pandas在API设计中遵循"Convention Over Configuration"方法。这意味着已经安装了相同的API,以适应广泛的数据和用例。

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

  1. 索引/轴对齐
  2. 处理混合数据类型
  3. 处理丢失的数据

几乎每个功能都必须在不同程度上处理这些问题,这带来了开销。数字函数(例如,Series.add)的开销较小,而字符串函数(例如,Series.str.replace)的开销更为明显。

另一方面,

for循环比您想象的要快。更好的是list comprehensions(通过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测试。以上操作的时间如下:

enter image description here

对于中等大小的N,列表理解要胜过query,对于较小的N,列表理解甚至要胜过向量化不等于比较。不幸的是,列表理解线性地缩放,因此对于较大的N来说并不能提供太多的性能提升。< / p>

  

注意
  值得一提的是,列表理解的大部分好处来自不必担心索引对齐,   但这意味着,如果您的代码依赖于索引对齐,   这会打破。在某些情况下,   底层的NumPy数组可被视为带来“最佳   两个世界”,无需向量化即可 无需使用熊猫函数的所有不必要的开销。这意味着您可以将上面的操作重写为

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

哪个比熊猫和列表理解等效项都好:
  
  NumPy向量化不在本文的讨论范围内,但是如果性能很重要,则绝对值得考虑。

价值计数
再举一个例子-这次,它的另一个香草python结构比for循环更快更快-collections.Counter。通常的要求是计算值计数并将结果作为字典返回。这是通过value_countsnp.uniqueCounter完成的:

# 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

enter image description here

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

  

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

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

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

enter image description here

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


混合/ object dtypes的操作

基于字符串的比较
再来看第一部分的过滤示例,如果要比较的列是字符串怎么办?考虑上面相同的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

enter image description here

那么,什么改变了?这里要注意的是字符串操作本来就难以向量化。 Pandas将字符串视为对象,并且对对象的所有操作都归结为缓慢,循环的实现。

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

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

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

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

enter image description here

位置列表索引
从列(处理异常),mapstr.get accessor method和列表理解中提取第0个元素的3个操作的时间:

# 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)
     

重建系列时。

enter image description here

列表拼合
最后一个例子是扁平化列表。这是另一个常见的问题,它演示了纯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

enter image description here

itertools.chain.from_iterable和嵌套列表理解都是纯python构造,并且扩展性比stack解决方案好。

这些时间点很明显地说明了熊猫没有配备混合dtypes的能力,您可能应该避免使用它来做到这一点。数据应尽可能在单独的列中作为标量值(整数/浮点数/字符串)显示。

最后,这些解决方案的适用性在很大程度上取决于您的数据。因此,最好的办法是在决定要使用的内容之前对数据进行这些操作测试。请注意,我还没有为apply设置这些解决方案的时间,因为它会使图形偏斜(是的,那太慢了)。


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

Pandas可以应用正则表达式操作,例如str.containsstr.extractstr.extractall,以及其他“矢量化”字符串操作(例如str.split,str.find , str.translate`,依此类推)在字符串列上。这些功能比列表理解要慢,并且是比其他功能更方便的功能。

预编译一个正则表达式模式并使用re.compile遍历数据通常要快得多(另请参阅Is it worth using Python's 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函数是非常可扩展的。根据需要,它可以适合返回每个捕获组的列表。只需提取查询匹配器对象的groupgroups属性即可。

对于str.extractall,将p.search更改为p.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

enter image description here

更多示例
完全披露-我是下面列出的这些帖子的部分或全部作者。


结论

如以上示例所示,当处理一小排DataFrame,混合数据类型和正则表达式时,迭代会发光。

您获得的提速取决于您的数据和问题,因此里程可能会有所不同。最好的办法是仔细运行测试,看看是否值得付出努力。

“矢量化”功能以其简单性和可读性着称,因此,如果性能不是很关键,则您绝对应该首选这些功能。

另一方面,某些字符串操作处理的约束都支持使用NumPy。以下是两个示例,其中仔细的NumPy向量化性能胜过python:

此外,有时仅通过.values在基础数组上进行操作,而不是在Series或DataFrames上进行操作,就可以为大多数常见情况提供足够健康的加速(请参见<上面的strong>数字比较部分)。因此,例如df[df.A.values != df.B.values]将比df[df.A != df.B]表现出即时的性能提升。使用.values可能并非在每种情况下都适用,但这是一个有用的技巧。

如上所述,您应自行决定这些解决方案是否值得实施。


附录:代码段

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
)

答案 1 :(得分:1)

简而言之

  • for循环+ iterrows非常慢。大约1k行的开销并不重要,但超过10k行的开销却很明显。
  • for循环+ itertuplesiterrowsapply快得多。
  • 向量化通常比itertuples
  • 快得多

基准 enter image description here