For loops with pandas - When should I care?
在我熟悉的概念,以及如何"矢量化"的大熊猫对UPS技术employs computation速度矢量。矢量函数飞越整个系列广播业务实现他们对多帧迭代速度比大conventionally飞越的日期。 >
然而,我相当惊讶地看到很多代码(包括在线提供从答案到问题解决方案栈overflow),involve环通环和列表中使用tldr;不,
让我们单独检查一下这些情况。好的。小数据的迭代V/S矢量化
PANDAS在其API设计中采用了"配置约定"方法。这意味着已经安装了相同的API来满足广泛的数据和用例。好的。
当调用panda函数时,函数必须在内部处理以下内容(除其他外),以确保工作好的。
几乎每个函数都必须在不同程度上处理这些问题,这会带来开销。数值函数(例如,
另一方面,
列表理解遵循模式好的。
1 | [f(x) for x in seq] |
其中,
1 | [f(x, y) for x, y in zip(seq1, seq2)] |
其中,
数值比较考虑一个简单的布尔索引操作。列表理解方法针对
1 2 3 4 | # 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 |
为了简单起见,我使用了
好的。
对于中等大小的n,列表理解优于
Note
It is worth mentioning that much of the benefit of list comprehension come from not having to worry about the index alignment,
but this means that if your code is dependent on indexing alignment,
this will break. In some cases, vectorised operations over the
underlying NumPy arrays can be considered as bringing in the"best of
both worlds", allowing for vectorisation without all the unneeded overhead of the pandas functions. This means that you can rewrite the operation above asOk.
1 df[df.A.values != df.B.values]Which outperforms both the pandas and list comprehension equivalents:
NumPy vectorization is out of the scope of this post, but it is definitely worth considering, if performance matters.Ok.
价值计数举另一个例子——这次,使用另一个比for循环更快的普通python构造——
1 2 3 4 | # 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),
Note
More trivia (courtesy @user2357112). TheCounter is implemented with a C
accelerator,
so while it still has to work with python objects instead of the
underlying C datatypes, it is still faster than afor loop. Python
power!Ok.
当然,这里要考虑的是性能取决于您的数据和用例。这些例子的要点是说服您不要将这些解决方案排除在合法选项之外。如果这些仍然不能给你你所需要的性能,总是有赛通和纽巴。让我们把这个测试加入到混合中。好的。
1 2 3 4 5 6 7 8 9 10 11 | 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工作涉及到一个学习曲线。好的。混合/
基于字符串的比较重新访问第一节中的筛选示例,如果要比较的列是字符串呢?考虑上面相同的3个函数,但是输入数据帧转换为字符串。好的。
1 2 3 4 | # 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将字符串视为对象,对象上的所有操作都会返回到缓慢、循环的实现中。好的。
现在,因为这个循环的实现被上面提到的所有开销所包围,所以这些解决方案之间存在一个恒定的量级差异,即使它们的规模相同。好的。
当涉及到可变/复杂对象的操作时,没有比较。列表理解优于所有涉及听写和列表的操作。好的。
按键访问字典值下面是从字典列中提取值的两个操作的时间安排:
1 2 3 | # Dictionary value extraction. ser.map(operator.itemgetter('value')) # map pd.Series([x.get('value') for x in ser]) # list comprehension |
好的。
位置列表索引从列列表中提取第0个元素(处理异常)、
1 2 3 4 5 6 7 | # List positional indexing. def get_0th(lst): try: return lst[0] # Handle empty lists and NaNs gracefully. except (IndexError, TypeError): return np.nan |
好的。
1 2 3 4 | 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 |
Note
If the index matters, you would want to do:Ok.
1 pd.Series([...], index=ser.index)When reconstructing the series.
Ok.
好的。
列表平坦化最后一个例子是扁平化列表。这是另一个常见问题,并演示了纯Python的强大功能。好的。
1 2 3 4 | # 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 |
好的。
这些时间是一个强有力的迹象,表明熊猫不具备与混合数据类型一起工作的能力,并且您可能应该避免使用它来完成这项工作。在可能的情况下,数据应该作为标量值(ints/floats/strings)出现在单独的列中。好的。
最后,这些解决方案的适用性很大程度上取决于您的数据。所以,最好的做法是在决定使用什么之前对数据测试这些操作。注意,我没有在这些解决方案上对
pandas可以在字符串列上应用regex操作,如
预编译regex模式并使用
1 2 | p = re.compile(...) ser2 = pd.Series([x for x in ser if p.search(x)]) |
或者,好的。
1 | ser2 = ser[[bool(p.search(x)) for x in ser]] |
如果你需要照顾奶奶,你可以做些类似的事情好的。
1 | ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]] |
相当于
1 | df['col2'] = [p.search(x).group(0) for x in df['col']] |
如果不需要处理任何匹配项和nan,则可以使用自定义函数(速度更快!):好的。
1 2 3 4 5 6 7 | 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']] |
对于
字符串提取考虑一个简单的过滤操作。如果前面是大写字母,则提取4位数字。好的。
1 2 3 4 5 6 7 8 9 10 | # 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 |
好的。
更多例子完全披露-我是下面列出的这些文章的作者(部分或全部)。好的。
熊猫快速删除标点符号好的。
两个panda列的字符串连接好的。
从列中的字符串中删除不需要的部分好的。
替换数据帧中除最后一次出现的字符以外的所有字符好的。
结论
如上面的例子所示,迭代在处理小的数据帧行、混合数据类型和正则表达式时会发光。好的。
你得到的加速取决于你的数据和你的问题,所以你的里程数可能会有所不同。最好的做法是仔细运行测试,看看付出的代价是否值得。好的。
"矢量化"函数以其简单性和可读性著称,因此,如果性能不是关键的,您肯定会更喜欢这些函数。好的。
另一个注意事项是,某些字符串操作处理有利于使用numpy的约束。下面是两个例子,其中谨慎的numpy矢量化优于python:好的。
以更快、高效的方式使用增量值创建新列-Divakar回答好的。
用pandas快速删除标点符号-Paul Panzer回答好的。
此外,有时仅仅通过
如上所述,由您决定这些解决方案是否值得麻烦地实现。好的。附录:代码段
1 2 3 4 5 6 7 8 | import perfplot import operator import pandas as pd import numpy as np import re from collections import Counter from itertools import chain |
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 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' ) |
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 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) ) |
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 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 ) |
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 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 ) |
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # 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 ) |
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # 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 ) |
好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 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 ) |
好啊。