在不幸的情况下,循环 Pandas 数据帧的行是唯一的方法,通常会提到 itertuples() 在计算速度方面优于 iterrows()。此断言对于具有很少列的数据帧(“窄数据帧”)似乎有效,但对于具有数百列的宽数据帧似乎并非如此。
这正常吗?在按列数缩放方面,itertuples 的行为不应该像 iterrows 一样吗,即恒定时间而不是线性时间?
下面所附的代码片段显示了当数据帧宽度增加时,从 itertuples() 到 iterrows() 的交叉是最快的迭代方法。
import pandas as pd
import time
from pylab import *
size_range = [100, 300, 600, 1200]
nrows = 100000
compute_time_rows = zeros(len(size_range))
compute_time_tuples = zeros(len(size_range))
for s, size in enumerate(size_range):
x = pd.DataFrame(randn(nrows, size))
start = time.time()
for idx, row in x.iterrows(): z = sum(row)
stop = time.time()
compute_time_rows[s] = stop - start
start = time.time()
for row in x.itertuples(index=False): z = sum(row)
stop = time.time()
compute_time_tuples[s] = stop - start
xlabel('Dataframe width')
ylabel('Computation time [s]')
pd.Series(compute_time_rows, index=size_range).plot(grid=True, label='iterrows')
pd.Series(compute_time_tuples, index=size_range).plot(grid=True, label='itertuples')
title(f'Iteration over a {nrows} rows Pandas dataframe')
legend()
[行迭代速度与数据帧宽度] https://i.sstatic.net/65QuPfjB.png
首先,你不应该使用它们中的任何一个。 无论您如何执行,在 Python 循环中迭代数据帧的速度都非常慢。 仅当您不关心性能时才可以使用它们。 话虽这么说,我会根据我的技术好奇心来回答你的问题。
这两个API都是Python实现的辅助函数,大家可以看一下。
以下是相关部分。如果您愿意,您可以检查这些显示的图案是否与原始图案相同。
import pandas as pd
def iterrows(df: pd.DataFrame):
for row in df.values:
yield pd.Series(row)
def itertuples(df: pd.DataFrame):
cols = [df.iloc[:, k] for k in range(len(df.columns))]
return zip(*cols)
从上面的代码中,我们可以发现两个主要因素。
第一件事是 iterrows 不复制(在本例中)。
df.values
返回单个 numpy 数组。如果对其进行迭代,每一行都是原始数组的视图,而不是副本。
当您将 numpy 数组传递给 pd.Series
时,它会将其用作内部缓冲区,因此它也是一个视图。
因此, iterrows 产生数据帧的视图。
另一方面,itertuples 首先创建列视图,但将其余部分留给 Python 的内置 zip
函数,该函数会进行复制。
其次,由于
zip
ing 列相当于为每一列创建一个迭代器,因此迭代开销会成倍增加(与 iterrow 相比)。
所以 itertuples 不仅是线性的,而且速度也很慢。
公平地说,我不认为 itertuples 被设计来处理如此大量的列,因为它对于少量的列要快得多。
为了证实这个假设,我将展示两个例子。 第一个,在 iterrows 中,产生一个
tuple
而不是 pd.Series
。
这使得 iterrows 呈线性,因为它强制复制。
def iterrows_2(df: pd.DataFrame):
for row in df.values:
# yield pd.Series(row)
yield tuple(row)
通过这个简单的更改,iterrows 的性能特征现在与 itertuples 的性能特征非常相似(但不知何故更快)。
第二个,以 itertuples 形式,转换为单个 numpy 数组,而不是使用
zip
。
def itertuples_2(df: pd.DataFrame):
cols = [df.iloc[:, k] for k in range(len(df.columns))]
# return zip(*cols)
return np.array(cols).T # This is only possible for the single dtype dataframe.
创建
cols
并将其转换为 numpy 数组都需要线性时间,因此总体上仍然是线性的,但速度要快得多。
换句话说,您可以看到zip(*cols)
有多慢。
这是基准代码:
import time
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
def iterrows(df: pd.DataFrame):
for row in df.values:
yield pd.Series(row)
def iterrows_2(df: pd.DataFrame):
for row in df.values:
# yield pd.Series(row)
yield tuple(row)
def itertuples(df: pd.DataFrame):
cols = [df.iloc[:, k] for k in range(len(df.columns))]
return zip(*cols)
def itertuples_2(df: pd.DataFrame):
cols = [df.iloc[:, k] for k in range(len(df.columns))]
# return zip(*cols)
return np.array(cols).T # This is only possible for the single dtype dataframe.
def benchmark(candidates):
size_range = [100, 300, 600, 900, 1200]
nrows = 100000
times = {k: [] for k in candidates}
for size in size_range:
for func in candidates:
x = pd.DataFrame(np.random.randn(nrows, size))
start = time.perf_counter()
for row in func(x):
pass
stop = time.perf_counter()
times[func].append(stop - start)
# Plot
plt.figure()
for func in candidates:
s = pd.Series(times[func], index=size_range, name=func.__name__)
plt.plot(s.index, s.values, marker="o", label=s.name)
plt.title(f"iterrows vs itertuples ({nrows} rows)")
plt.xlabel("Columns")
plt.ylabel("Time [s]")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
benchmark([iterrows, itertuples, iterrows_2])
benchmark([iterrows, itertuples, itertuples_2])
仅供参考,请注意 iterrows 会在您的基准测试中产生一个视图,但情况并非总是如此。 例如,如果将第一列转换为字符串类型,它将进行复制,并且 iterrows 将花费线性时间。
x = pd.DataFrame(np.random.randn(nrows, size))
x[0] = x[0].astype(str)