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