pandas 性能:列选择

Dan*_*l R 5 pandas

我今天观察到,选择两列或更多列数据框可能比只选择一列慢得多。

如果我使用 loc 或 iloc 选择多个列,并使用 list 传递列名或索引,则与使用 iloc 选择单列或多列相比,性能下降 100 倍(但未通过列表)

例子:

df = pd.DataFrame(np.random.randn(10**7,10), columns=list('abcdefghij'))
Run Code Online (Sandbox Code Playgroud)

一栏选择:

%%timeit -n 100
df['b']
3.17 µs ± 147 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

%%timeit -n 100
df.iloc[:,1]
66.7 µs ± 5.95 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%%timeit -n 100
df.loc[:,'b']
44.2 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Run Code Online (Sandbox Code Playgroud)

两列选择:

%%timeit -n 10
df[['b', 'c']]
96.4 ms ± 788 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.loc[:,['b', 'c']]
99.4 ms ± 4.44 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.iloc[:,[1,2]]
97.6 ms ± 1.79 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)

只有这个选择像预期的那样工作:[编辑]

%%timeit -n 100
df.iloc[:,1:3]
103 µs ± 17.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Run Code Online (Sandbox Code Playgroud)

机制有什么不同,为什么它们如此之大?

[编辑]:正如@run-out 指出的,pd.Series 的处理速度似乎比 pd.DataFrame 快得多,有人知道为什么会这样吗?

另一方面 - 它没有解释df.iloc[:,[1,2]]df.iloc[:,1:3]

run*_*out 7

Pandas 将单行或单列作为 pandas.Series 使用,这比在 DataFrame 架构中工作要快。

当您要求时,Pandas 可与 pandas.Series 配合使用:

%%timeit -n 10
df['b']
2.31 µs ± 1.59 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)

但是,我可以通过将它放在列表中来调用同一列的 DataFrame。然后你得到:

%%timeit -n 10
df[['b']]
90.7 ms ± 1.73 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)

从上面可以看出,Series 的表现优于 DataFrame。

以下是 Pandas 如何处理“b”列。

type(df['b'])
pandas.core.series.Series

type(df[['b']])
pandas.core.frame.DataFrame
Run Code Online (Sandbox Code Playgroud)

编辑:我正在扩展我的答案,因为 OP 想要深入研究为什么 pd.series 与 pd.dataframe 的速度更快。而且因为这是一个很好的问题,可以扩展我/我们对底层技术如何工作的理解。请有更多专业知识的人加入。

首先让我们从 numpy 开始,因为它是 Pandas 的构建块。根据 Pandas 的作者和 Python for Data Analysis 的作者 Wes McKinney 的说法,numpy 的性能比 python 好:

This is based partly on performance differences having to do with the
cache hierarchy of the CPU; operations accessing contiguous blocks of memory (e.g.,
summing the rows of a C order array) will generally be the fastest because the mem?
ory subsystem will buffer the appropriate blocks of memory into the ultrafast L1 or
L2 CPU cache. 
Run Code Online (Sandbox Code Playgroud)

让我们看看这个例子的速度差异。让我们从数据框的“b”列创建一个 numpy 数组。

a = np.array(df['b'])
Run Code Online (Sandbox Code Playgroud)

现在做性能测试:

%%timeit -n 10
a
Run Code Online (Sandbox Code Playgroud)

结果是:

32.5 ns ± 28.2 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)

在 2.31 µs 的 pd.series 时间内,这是性能的严重提升。

性能提升的另一个主要原因是 numpy 索引直接进入 NumPy C 扩展,但是当你索引到一个系列时有很多 python 东西在运行,这要慢得多。(阅读这篇文章

让我们来看看为什么会这样的问题:

df.iloc[:,1:3]
Run Code Online (Sandbox Code Playgroud)

大幅超越:

df.iloc[:,[1,2]]
Run Code Online (Sandbox Code Playgroud)

有趣的是,在这种情况下,.loc 对性能的影响与 .iloc 相同。

我们的第一个重要线索是在以下代码中:

df.iloc[:,1:3] is df.iloc[:,[1,2]]
False
Run Code Online (Sandbox Code Playgroud)

这些给出了相同的结果,但是是不同的对象。我进行了深入研究,试图找出不同之处。我无法在互联网或我的图书馆中找到对此的参考。

查看源代码,我们可以开始看到一些差异。我指的是 indexing.py。

在 _iLocIndexer 类中,我们可以找到熊猫为 iloc 切片中的列表所做的一些额外工作。

马上,我们在检查输入时遇到了这两个区别:

if isinstance(key, slice):
            return
Run Code Online (Sandbox Code Playgroud)

对比

elif is_list_like_indexer(key):
            # check that the key does not exceed the maximum size of the index
            arr = np.array(key)
            l = len(self.obj._get_axis(axis))

            if len(arr) and (arr.max() >= l or arr.min() < -l):
                raise IndexError("positional indexers are out-of-bounds")
Run Code Online (Sandbox Code Playgroud)

仅此一项就足以导致性能下降吗?我不知道。

尽管 .loc 略有不同,但在使用值列表时也会影响性能。查看index.py,查看def _getitem_axis(self, key, axis=None): --> in class _LocIndexer(_LocationIndexer):

处理列表输入的 is_list_like_indexer(key) 的代码部分很长,包括很多开销。它包含注释:

# convert various list-like indexers
# to a list of keys
# we will use the *values* of the object
# and NOT the index if its a PandasObject
Run Code Online (Sandbox Code Playgroud)

当然,在处理值或整数列表时有足够的额外开销,然后直接切片导致处理延迟。

其余的代码超过了我的工资等级。如果有人可以看看并敲响它,那将是最受欢迎的

  • 实际上,np.array 的部分是误导性的。通过调用“%%timeit -n 10 a”,它从内存中读取,同时调用“df['b']”创建 pd.Series。参见:`a = df['b']`,`b = np.array(df['b'])`,对于`a`和`b`,读入需要相同的时间(在我的情况 16 纳秒)。这对我来说很有意义,因为 pd.Series 基本上是 np.array (+元数据)。更重要的是:`c=df[['a','b']]', ``%%timeit c` 每个循环给出 17.9 ns ± 0.0663 ns,仅稍微多一点。DataFrame 可能仍然只是底层的数组 (2认同)