我试图了解 Pandas 中iloc函数的执行复杂度是多少。我阅读了以下 Stack Exchange 线程(Pandas DataFrame 搜索是线性时间还是常数时间?):
“按索引访问单行(索引已排序且唯一)应具有运行时 O(m) 其中
m << n_rows”
提到按时iloc运行O(m)。什么是m(线性,对数,常数,...)?
我运行的一些实验:
import pandas as pd
>>> a = pd.DataFrame([[1,2,3],[1,3,4],[2,3,4],[2,4,5]], columns=['a','b','c'])
>>> a = a.set_index('a').sort_index()
>>> a
b c
a
1 3 4
1 4 5
2 2 3
2 3 4
>>> a.iloc[[0,1,2,3]]
b c
a
1 3 4
1 4 5
2 2 3
2 3 4
Run Code Online (Sandbox Code Playgroud)
所以iloc显然适用于偏移量而不是基于整数的索引(列a)。即使我们删除顶部的几行,iloc基于偏移量的查找也能正常工作:
>>> a.drop([1]).iloc[[0,1]]
b c
a
2 2 3
2 3 4
Run Code Online (Sandbox Code Playgroud)
那么,iloc当每一列只是一个可以在恒定时间(很少操作)访问的 numpy 数组时,为什么偏移查找的运行时间与 numpy 数组相当?它的复杂性是什么?谢谢
更新:
我试图在 10000000x2 矩阵上比较 pandas 与 numpy 的效率。比较 DataFramedf和 array 中每行值增量的效率,arr有for循环和没有循环:
#Initialization
SIZE = 10000000
arr = np.ones((SIZE,2), dtype=np.uint32)
df = pd.DataFrame(arr)
#numpy, no for loop
arr[range(SIZE),1]+=1
#pandas, no for loop
df.iloc[range(SIZE),1]+=1
#numpy, for loop
for i in range(SIZE):
arr[i,1]+=1
#pandas, for loop
for i in range(SIZE):
df.iloc[i,1]+=1
Run Code Online (Sandbox Code Playgroud)
执行次数:
对于iloc. 该方法接受大量的输入类型,而这种灵活性必然会带来成本。这些成本可能包括较大的恒定因素和非常数成本,这些成本几乎肯定取决于其使用方式。
回答您的问题的一种方法是逐步执行这两种情况下的代码。
range首先,使用range(SIZE). 假设df按照您的方式定义,您可以运行:
import pdb
pdb.run('df.iloc[range(SIZE), 1]')
Run Code Online (Sandbox Code Playgroud)
然后s逐步执行代码以遵循路径。最终,这到达了这一行:
self._values.take(indices)
Run Code Online (Sandbox Code Playgroud)
其中indices是从原始 构造的整数 ndarray range,self._values是数据帧的源 ndarray 。
对此有两点需要注意。首先,范围被具体化为 ndarray,这意味着您至少有SIZE元素的内存分配。所以...这会花费你一些时间:)。我不知道 NumPy 本身的索引是如何发生的,但考虑到您生成的时间测量,可能没有发生(或发生更小的分配)。
第二件事要注意的是numpy.take制作副本。您可以通过查看调用此方法返回的对象的属性来验证这一点.flags,该属性表明它拥有其数据并且不是原始数据的视图。(另请注意,np.may_share_memory返回False。)所以...还有另一个分配:)。
结论:这里并不明显存在任何非线性运行时间,但显然存在很大的常数因子。多次分配可能是最大的杀手,但是属性下的调用树中复杂的分支逻辑.iloc肯定没有帮助。
这条路径中的代码要短得多。它很快就到达这里:
return self.obj._get_value(*key, takeable=self._takeable)
Run Code Online (Sandbox Code Playgroud)
这里真正糟糕的运行时可能是由于元组解包造成的。在每次循环迭代中,重复地解包和重新打包key为元组。(请注意,这已经是 tuple ,所以这很糟糕。接受通用可迭代的成本。)key(i, 1)
无论如何,我们可以通过分析来估计特定用例的实际运行时间。以下脚本将生成一个以日志间隔排列的数组大小列表,数组大小为 10 到 10e9,索引范围为,并打印出运行该__getitem__方法所需的时间。(树中只有两个这样的调用,因此很容易看出哪一个是我们关心的。)
import pandas as pd
import numpy as np
import cProfile
import pstats
sizes = [10 ** i for i in range(1, 9)]
for size in sizes:
df = pd.DataFrame(data=np.zeros((size, 2)))
with cProfile.Profile() as pr:
pr.enable()
df.iloc[range(size), 1]
pr.disable()
stats = pstats.Stats(pr)
print(size)
stats.print_stats("__getitem__")
Run Code Online (Sandbox Code Playgroud)
一旦输出达到最小分辨率,您可以在此处看到非常清晰的线性行为:
Size | Runtime
------------------
10000 | 0.002
100000 | 0.021
1000000 | 0.206
10000000 | 2.145
100000000| 24.843
Run Code Online (Sandbox Code Playgroud)
所以我不确定你指的是哪些来源来谈论索引的非线性运行时。它们可能已经过时,或者考虑使用与使用range.