Pandas中.iloc[]的计算复杂度及与NumPy性能差异疑问
最近我一直在纠结Pandas里iloc函数的执行复杂度问题,之前在Stack Exchange的帖子里看到这么一句话:
通过索引访问单行(索引已排序且唯一)的运行时间应为O(m),其中m << n_rows
这里提到iloc的时间复杂度是O(m),可我一直搞不懂这个m到底指代什么——是线性、对数还是常数级别的?
先做个小实验确认iloc的工作逻辑
我先写了段简单代码验证iloc的行为:
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() print(a) # 输出: # b c # a # 1 3 4 # 1 4 5 # 2 2 3 # 2 3 4 # 用iloc按偏移量取所有行 print(a.iloc[[0,1,2,3]]) # 输出和原DataFrame完全一致 # 删除部分行后再用iloc print(a.drop([1]).iloc[[0,1]]) # 输出: # b c # a # 2 2 3 # 2 3 4
从结果能明确:iloc是基于位置偏移量工作的,和DataFrame的自定义索引(比如这里的列a)毫无关系——哪怕索引被修改、行被删除,它只认当前DataFrame里的位置序号。
核心疑问:为啥iloc不如NumPy数组快?
既然DataFrame的每一列本质都是可以常数时间访问的NumPy数组,那为啥iloc的偏移查找性能没法和纯NumPy比?它的真实复杂度到底是多少?
大规模效率对比测试
为了搞清楚这个问题,我在1000万行×2列的数据集上做了Pandas和NumPy的效率对比,测试了逐行递增数值的两种场景——有无for循环:
import numpy as np import pandas as pd SIZE = 10000000 arr = np.ones((SIZE,2), dtype=np.uint32) df = pd.DataFrame(arr) # numpy, 无for循环 arr[range(SIZE),1] += 1 # pandas, 无for循环 df.iloc[range(SIZE),1] += 1 # numpy, 有for循环 for i in range(SIZE): arr[i,1] += 1 # pandas, 有for循环 for i in range(SIZE): df.iloc[i,1] += 1
测试出来的执行时间差得非常明显:
| 方法 | 执行时间 |
|---|---|
| numpy, 无for循环 | 7秒 |
| pandas, 无for循环 | 24秒 |
| numpy, 有for循环 | 27秒 |
| pandas, 有for循环 | >2小时 |
搞清楚复杂度和性能差异的原因
关于O(m)里的m
首先解释之前看到的O(m):这里的m其实是你要访问的元素/行的数量,n_rows是DataFrame的总行数。iloc在处理访问请求时,底层确实是调用NumPy数组的常数时间访问,但Pandas多了很多额外的封装工作:
- 校验输入的偏移量是否合法(比如有没有超出DataFrame的行数/列数)
- 处理各种输入类型(单个整数、列表、切片、布尔数组等)
- 维护DataFrame的元数据(索引、列名、数据类型等),确保返回的结果依然是符合Pandas规范的对象
这些额外操作都会带来开销,这就是iloc比纯NumPy慢的核心原因。
真实的时间复杂度
- 当访问单个元素/单行时:复杂度是O(1)——底层还是直接访问NumPy数组的对应位置,只是多了一层Pandas的校验和封装,这部分开销很小。
- 当批量访问m个元素/行时:复杂度是O(m)——Pandas需要逐个处理每个位置的请求,加上前面说的额外开销,所以比纯NumPy的O(m)要慢,但依然是线性级别的。
为啥for循环里iloc慢到离谱?
这是因为每次循环调用iloc[i,1]时,Pandas都要重新执行一遍索引校验、上下文维护这些额外工作——相当于把O(1)操作里的那点小开销重复了1000万次,累积起来就变成了灾难级的耗时。而NumPy的arr[i,1]几乎没有额外开销,所以哪怕用循环也快很多。
内容的提问来源于stack exchange,提问作者Bruno Maga

