用熊猫循环数据帧的最有效方法是什么?

我想对数据框架中的财务数据按顺序执行自己的复杂操作。

例如,我正在使用以下MSFT CSV文件从雅虎财经:

Date,Open,High,Low,Close,Volume,Adj Close
2011-10-19,27.37,27.47,27.01,27.13,42880000,27.13
2011-10-18,26.94,27.40,26.80,27.31,52487900,27.31
2011-10-17,27.11,27.42,26.85,26.98,39433400,26.98
2011-10-14,27.31,27.50,27.02,27.27,50947700,27.27


....

然后我做以下事情:

#!/usr/bin/env python
from pandas import *


df = read_csv('table.csv')


for i, row in enumerate(df.values):
date = df.index[i]
open, high, low, close, adjclose = row
#now perform analysis on open/close based on date, etc..

这是最有效的方法吗?考虑到在pandas中对速度的关注,我认为必须有一些特殊的函数以一种也检索索引的方式遍历值(可能通过生成器来提高内存效率)?df.iteritems不幸的是只逐列迭代。

638187 次浏览

Pandas基于NumPy数组 提高NumPy数组速度的关键是一次对整个数组执行操作,而不是逐行或逐项执行

例如,如果close是一个一维数组,而你想要逐日变化的百分比,

pct_change = close[1:]/close[:-1]

这将计算整个百分比变化数组作为一个语句,而不是

pct_change = []
for row in close:
pct_change.append(...)
所以尽量完全避免Python循环for i, row in enumerate(...),并且 考虑如何在整个数组(或数据帧)上执行运算,而不是逐行执行运算

你可以通过换位然后调用iteritems来遍历这些行:

for date, row in df.T.iteritems():
# do some logic here

我对这种情况下的效率没有把握。为了在迭代算法中获得最好的性能,你可能想要探索在Cython中编写它,所以你可以这样做:

def my_algo(ndarray[object] dates, ndarray[float64_t] open,
ndarray[float64_t] low, ndarray[float64_t] high,
ndarray[float64_t] close, ndarray[float64_t] volume):
cdef:
Py_ssize_t i, n
float64_t foo
n = len(dates)


for i from 0 <= i < n:
foo = close[i] - open[i] # will be extremely fast

我建议先用纯Python编写算法,确保它能工作,然后看看它有多快——如果不够快,就用最小的工作量把东西转换成这样的Cython,以得到与手工编写的C/ c++差不多快的东西。

最新版本的pandas现在包含了一个用于遍历行的内置函数。

for index, row in df.iterrows():


# do some logic here

或者,如果你想更快地使用itertuples()

但是,unutbu建议使用numpy函数来避免遍历行,这会产生最快的代码。

在注意到尼克·克劳福德的的答案后,我检查了iterrows,但发现它产生(index, Series)元组。不确定哪个最适合你,但我最终使用itertuples方法来解决我的问题,它会产生(index, row_value1…)元组。

还有iterkv,它遍历(column, series)元组。

作为一个小的补充,如果你有一个复杂的函数,你可以应用到一个单列:

http://pandas.pydata.org/pandas-docs/dev/generated/pandas.DataFrame.apply.html < a href = " http://pandas.pydata.org/pandas-docs/dev/generated/pandas.DataFrame.apply.html " > < / >

df[b] = df[a].apply(lambda col: do stuff with col here)

另一个建议是将groupby与向量化计算结合起来,如果行的子集共享允许这样做的特征。

就像之前提到的,pandas对象在一次处理整个数组时是最有效的。然而,对于那些真正需要通过pandas DataFrame循环来执行某些事情的人,比如我,我发现至少有三种方法可以做到这一点。我做了一个简短的测试,看看三种方法中哪一种最省时。

t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
B = []
C = []
A = time.time()
for i,r in t.iterrows():
C.append((r['a'], r['b']))
B.append(time.time()-A)


C = []
A = time.time()
for ir in t.itertuples():
C.append((ir[1], ir[2]))
B.append(time.time()-A)


C = []
A = time.time()
for r in zip(t['a'], t['b']):
C.append((r[0], r[1]))
B.append(time.time()-A)


print B

结果:

[0.5639059543609619, 0.017839908599853516, 0.005645036697387695]

这可能不是衡量时间消耗的最佳方法,但对我来说很快。

以下是我个人认为的一些优点和缺点:

  • .iterrows():在单独的变量中返回索引和行项,但速度明显较慢
  • .itertuples():比.iterrows()快,但返回索引和行项,ir[0]是索引
  • Zip:最快,但不能访问行的索引

编辑2020/11/10

为了它的价值,这里是一个更新的基准测试与其他一些替代方案(性能与MacBookPro 2,4 GHz英特尔酷睿i9 8核32 Go 2667 MHz DDR4)

import sys
import tqdm
import time
import pandas as pd


B = []
t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
for _ in tqdm.tqdm(range(10)):
C = []
A = time.time()
for i,r in t.iterrows():
C.append((r['a'], r['b']))
B.append({"method": "iterrows", "time": time.time()-A})


C = []
A = time.time()
for ir in t.itertuples():
C.append((ir[1], ir[2]))
B.append({"method": "itertuples", "time": time.time()-A})


C = []
A = time.time()
for r in zip(t['a'], t['b']):
C.append((r[0], r[1]))
B.append({"method": "zip", "time": time.time()-A})


C = []
A = time.time()
for r in zip(*t.to_dict("list").values()):
C.append((r[0], r[1]))
B.append({"method": "zip + to_dict('list')", "time": time.time()-A})


C = []
A = time.time()
for r in t.to_dict("records"):
C.append((r["a"], r["b"]))
B.append({"method": "to_dict('records')", "time": time.time()-A})


A = time.time()
t.agg(tuple, axis=1).tolist()
B.append({"method": "agg", "time": time.time()-A})


A = time.time()
t.apply(tuple, axis=1).tolist()
B.append({"method": "apply", "time": time.time()-A})


print(f'Python {sys.version} on {sys.platform}')
print(f"Pandas version {pd.__version__}")
print(
pd.DataFrame(B).groupby("method").agg(["mean", "std"]).xs("time", axis=1).sort_values("mean")
)


## Output


Python 3.7.9 (default, Oct 13 2020, 10:58:24)
[Clang 12.0.0 (clang-1200.0.32.2)] on darwin
Pandas version 1.1.4
mean       std
method
zip + to_dict('list')  0.002353  0.000168
zip                    0.003381  0.000250
itertuples             0.007659  0.000728
to_dict('records')     0.025838  0.001458
agg                    0.066391  0.007044
apply                  0.067753  0.006997
iterrows               0.647215  0.019600

正如@joris指出的那样,iterrowsitertuples慢得多,itertuplesiterrows快大约100倍,我在一个有500万条记录的数据帧中测试了这两种方法的速度,结果是iterrows,它是1200it/s,而itertuples是120000it/s。

如果使用itertuples,请注意for循环中的每个元素都是命名元组,因此要获得每个列的值,可以参考以下示例代码

>>> df = pd.DataFrame({'col1': [1, 2], 'col2': [0.1, 0.2]},
index=['a', 'b'])
>>> df
col1  col2
a     1   0.1
b     2   0.2
>>> for row in df.itertuples():
...     print(row.col1, row.col2)
...
1, 0.1
2, 0.2

你有三个选择:

通过指数(最简单):

>>> for index in df.index:
...     print ("df[" + str(index) + "]['B']=" + str(df['B'][index]))

使用iterrows(最常用):

>>> for index, row in df.iterrows():
...     print ("df[" + str(index) + "]['B']=" + str(row['B']))

使用itertuples(最快):

>>> for row in df.itertuples():
...     print ("df[" + str(row.Index) + "]['B']=" + str(row.B))

有三个选项显示如下内容:

df[0]['B']=125
df[1]['B']=415
df[2]['B']=23
df[3]['B']=456
df[4]['B']=189
df[5]['B']=456
df[6]['B']=12

来源:alphons.io

当然,遍历数据帧最快的方法是通过df.values访问底层numpy ndarray(就像你所做的那样),或者通过分别访问每一列df.column_name.values。因为你也想访问索引,你可以使用df.index.values

index = df.index.values
column_of_interest1 = df.column_name1.values
...
column_of_interestk = df.column_namek.values


for i in range(df.shape[0]):
index_value = index[i]
...
column_value_k = column_of_interest_k[i]

不是神谕的吗?当然。但很快。

如果你想从循环中挤出更多的果汁,你会想要查看cython。Cython将让你获得巨大的加速(想想10 -100倍)。为最大性能检查cython的内存视图

我相信循环dataframe最简单有效的方法是使用numpy和numba。在这种情况下,循环在许多情况下可以近似地与向量化操作一样快。如果numba不是一个选项,那么普通numpy可能是下一个最佳选项。正如已经多次提到的,您的默认值应该是向量化的,但是这个答案仅仅考虑了有效的循环,无论出于什么原因决定了循环。

对于测试用例,让我们使用@DSM回答的计算百分比变化的示例。这是一个非常简单的情况,作为一个实际问题,你不会写一个循环来计算它,但这样它为时间向量化方法和循环提供了一个合理的基线。

让我们用一个小的DataFrame来设置这4种方法,下面我们将在一个更大的数据集上对它们进行计时。

import pandas as pd
import numpy as np
import numba as nb


df = pd.DataFrame( { 'close':[100,105,95,105] } )


pandas_vectorized = df.close.pct_change()[1:]


x = df.close.to_numpy()
numpy_vectorized = ( x[1:] - x[:-1] ) / x[:-1]
        

def test_numpy(x):
pct_chng = np.zeros(len(x))
for i in range(1,len(x)):
pct_chng[i] = ( x[i] - x[i-1] ) / x[i-1]
return pct_chng


numpy_loop = test_numpy(df.close.to_numpy())[1:]


@nb.jit(nopython=True)
def test_numba(x):
pct_chng = np.zeros(len(x))
for i in range(1,len(x)):
pct_chng[i] = ( x[i] - x[i-1] ) / x[i-1]
return pct_chng
    

numba_loop = test_numba(df.close.to_numpy())[1:]

下面是100,000行的DataFrame上的计时(使用Jupyter的%timeit函数执行的计时,为便于阅读,折叠为摘要表):

pandas/vectorized   1,130 micro-seconds
numpy/vectorized      382 micro-seconds
numpy/looped       72,800 micro-seconds
numba/looped          455 micro-seconds

总结:对于简单的情况,比如这个例子,为了简单和可读性,可以使用(向量化的)pandas,为了速度,可以使用(向量化的)numpy。如果您确实需要使用循环,请使用numpy。如果numba可用,可以将其与numpy结合使用以获得更高的速度。在这种情况下,numpy + numba几乎和向量化numpy代码一样快。

其他细节:

  • 没有显示各种选项,如iterrows, itertuples等,这些选项要慢几个数量级,真的不应该使用。
  • 这里的计时相当典型:numpy比pandas快,向量化比循环快,但是将numba添加到numpy通常会显著加快numpy的速度。
  • 除了pandas选项外,其他选项都需要将DataFrame列转换为numpy数组。该转换包含在计时中。
  • 定义/编译numpy/numba函数的时间不包括在计时中,但对于任何大型数据帧来说,通常都是可以忽略不计的计时组成部分。

看最后一个

t = pd.DataFrame({'a': range(0, 10000), 'b': range(10000, 20000)})
B = []
C = []
A = time.time()
for i,r in t.iterrows():
C.append((r['a'], r['b']))
B.append(round(time.time()-A,5))


C = []
A = time.time()
for ir in t.itertuples():
C.append((ir[1], ir[2]))
B.append(round(time.time()-A,5))


C = []
A = time.time()
for r in zip(t['a'], t['b']):
C.append((r[0], r[1]))
B.append(round(time.time()-A,5))


C = []
A = time.time()
for r in range(len(t)):
C.append((t.loc[r, 'a'], t.loc[r, 'b']))
B.append(round(time.time()-A,5))


C = []
A = time.time()
[C.append((x,y)) for x,y in zip(t['a'], t['b'])]
B.append(round(time.time()-A,5))
B


0.46424
0.00505
0.00245
0.09879
0.00209