pandas 效率究竟如何?瓶颈剖析与优化实践
作者:KAKAKA2026.11.07 00:40浏览量:0简介:pandas 处理百万行、1GB 以内的数据时,效率与易用性俱佳;数据量继续增大后,逐行 apply、高精度类型与整文件加载会成为明显瓶颈。本文剖析这些慢操作的底层根源,并系统给出向量化计算、类型降精度、category 转换、分块加载等可落地的优化方案,帮助你成倍提升处理速度、大幅降低内存占用。
pandas 是 Python 生态中最常用的结构化数据处理库,做过数据分析的开发者几乎都绕不开它。但“pandas 到底快不快”这个问题,从来不能脱离数据规模与使用方式来回答:同一段代码,在几十万行的小表上瞬间完成,换到千万行的大表上可能十几分钟都跑不完。本文先厘清 pandas 的效率边界,再逐个拆解拖慢它的典型操作,最后给出一套可直接落地的优化方案。
一、先划边界:pandas 的效率要分场景看
pandas 的底层是 NumPy 数组,核心数据结构 Series 与 DataFrame 本质上是对连续内存数组的封装,因此它在中小规模数据上的表现并不差。对于百万行以内、整体体积 1GB 以内的数据集,内存完全放得下,单核运算也不吃力,此时 pandas 在“速度”与“易用性”上的综合收益最大,与同类数据处理方案的实际差距几乎可以忽略——这正是它在日常分析场景中被广泛使用的原因。
但当数据超过 1GB,情况就不同了。一方面,高精度默认类型叠加字符串列的 object 存储,会让实际内存占用远超文件名义体积,内存吃紧后系统开始换页,性能出现断崖式下跌;另一方面,pandas 的绝大多数操作默认是单线程的,任凭 CPU 有多少个核,实际只有一个核在干活,groupby、apply、merge 这类复杂操作的计算时间会随数据量线性甚至更快地增长。换句话说,不是 pandas 本身“变弱了”,而是“单机 + 单线程 + 全量驻留内存”这个模式到了天花板。
要优化,先要弄清到底是哪些操作在拖后腿。
二、拖慢 pandas 的三类典型操作
1. 逐行 apply:最常见的性能杀手
习惯了 for 循环的开发者迁到 pandas 后,很容易顺手用 apply(axis=1) 对每一行执行函数。这种写法看似简洁,实际非常慢:apply 会为每一行构造一个 Series 对象,再以普通 Python 函数调用的方式逐行执行,全程无法摆脱解释器开销。哪怕函数体极简单,每一行的“固定成本”也一分不少。
做个粗略估算:假设单行处理只需 0.0001 秒,一千万行数据累计就是 1000 秒,接近 17 分钟——而同样的逻辑改写为向量化,往往几秒内就能完成。以“根据 math、english 两列分数给出评级”这类多字段判断为例,apply 的写法大致是:
def judge(row):if row['math'] > 90 and row['english'] > 90:return '优秀'return '一般'df['result'] = df.apply(judge, axis=1) # 逐行执行,非常慢
补充一点:如果确实必须逐行遍历,itertuples 通常比 iterrows 快不少,因为它把每行封装为命名元组而非 Series。但这仍只是“慢的里面挑快的”,不到万不得已不应选择逐行方案。
2. groupby 本身不慢,慢的是“自定义函数”
需要澄清一个常见误区:groupby 并非天生低效。sum、mean、count 这类内置聚合,底层走的是编译优化过的实现,在较大数据上速度也尚可接受。真正拖慢的是 groupby().apply(自定义函数) 的写法——它会退化为对每个分组各执行一次 Python 调用,分组越多,解释器开销越大。
# 推荐:优先使用内置聚合,整列批量完成df.groupby('province')['amount'].sum()# 避免:能不写自定义函数就不写df.groupby('province').apply(lambda g: complicated(g))
3. 循环内反复合并数据
另一个隐蔽的坑是在循环里不断 concat 增量数据。每次合并都会触发整块数据的复制,循环次数一多,整体开销就向平方量级靠拢,“越跑越慢”的感觉正来源于此。正确姿势是先把各部分收集进列表,最后一次性合并,让复制只发生一次:
frames = []for f in file_list:frames.append(pd.read_csv(f))df_all = pd.concat(frames, ignore_index=True) # 只做一次合并
三、第一招:向量化,把逐行计算交给底层批量执行
向量化是 pandas/NumPy 体系中最有效的提速手段:它把“逐行调用函数”变成“对整列做一次批量运算”,运算过程在底层编译代码中对连续内存循环完成,既省掉了逐行的解释器开销,又能充分利用 CPU 缓存。在常见场景下,获得一到两个数量级的提升并不稀奇。
二分支判断用 np.where:
import numpy as npcond = (df['math'] > 90) & (df['english'] > 90)df['result'] = np.where(cond, '优秀', '一般')
多分支判断用 np.select,一组表达式写完所有分支:
conditions = [(df['math'] > 90) & (df['english'] > 90),(df['math'] > 75) & (df['english'] > 75),]choices = ['优秀', '良好']df['result'] = np.select(conditions, choices, default='一般')
两个实用提示:一是条件组合必须用 &、| 并给每个条件加括号,不能用 and/or,否则对整个布尔列求值会直接报错;二是除判断逻辑外,大量常见需求都有现成的向量化写法——字符串处理用 .str 系列(如 .str.contains)、日期处理用 .dt 系列(如 .dt.year)、连续值分箱用 pd.cut、多列算术直接整列运算。写代码前先问一句“有没有内置的整列方法”,往往就是收益最大的一次优化。
四、第二招:数据类型瘦身,把内存占用降下来
pandas 默认给数值列分配 int64/float64 这类高精度类型,精度冗余就是内存浪费:int64 占 8 字节,int8 只占 1 字节,同一列年龄数据,前者比后者多占 8 倍内存。省内存不只是“省”本身:占用越低,换页与缓存失效越少,速度往往同步受益。
第一优先级是在加载时就指定类型,避免“先按默认类型装满内存、再回头转换”造成的中间峰值;同时用 usecols 只读取需要的列,宽表场景效果立竿见影:
dtype_opt = {'id': 'int32', 'age': 'int8', 'price': 'float32'}df = pd.read_csv('data.csv',dtype=dtype_opt,usecols=['id', 'age', 'price', 'province'])
降精度前务必确认取值范围,例如 int8 只能表示 -128~127,超范围会溢出。对已加载的列,也可以让 downcast 参数自动选择合适的低精度:
df['age'] = pd.to_numeric(df['age'], downcast='integer')df['price'] = pd.to_numeric(df['price'], downcast='float')
字符串列是另一个内存大户:object 类型为每一行保存一个完整字符串对象的引用。像“省份”这种取值高度重复的列,更合理的做法是转为 category 类型——只保留一份取值字典加一列整数编码,内存可大幅下降,后续 groupby 与字符串匹配也会更快:
df['province'] = df['province'].astype('category')
优化前后,用以下代码即可量化效果:
df.info(memory_usage='deep') # 查看整体与各列内存print(df.memory_usage(deep=True).sum(), 'bytes') # 汇总内存占用
五、第三招:分块加载,让大文件化整为零
read_csv 默认把整个文件一次性读入内存。几十 MB 的小文件无所谓,但如果文件有数 GB,内存可能直接被塞满,轻则触发换页变慢,重则报错中断。
pandas 自带的 chunksize 参数可以把大文件按行切成若干小块,返回一个可迭代的分块器,每一块都是一个几十 MB 级别的小 DataFrame。配合“块内先处理、块间再合并”的模式,几 GB 的文件就被拆解成了 N 个毫无压力的小任务:
parts = []for chunk in pd.read_csv('big_data.csv', chunksize=100_000):# 块内先过滤、再局部聚合,尽量缩小需要保留的中间结果piece = chunk.loc[chunk['amount'] > 0, ['province', 'amount']]parts.append(piece.groupby('province', as_index=False)['amount'].sum())merged = pd.concat(parts, ignore_index=True)result = merged.groupby('province', as_index=False)['amount'].sum()
两个细节值得注意:其一,分块处理天然适合“过滤 + 聚合”类任务;如果任务强依赖全量数据(如全局排序、跨块去重后再复杂关联),需要设计两阶段的中间结果方案。其二,chunksize 指的是行数,单块实际体积取决于表有多宽,搭配 usecols 裁掉无关列,效果更好。
六、让数据说话:建立“测量—优化—对比”的习惯
优化最忌凭感觉。动手前先计时、看内存,改完再测一轮,提升才可信:
import timestart = time.perf_counter()df['result'] = np.select(conditions, choices, default='一般')print(f'耗时 {time.perf_counter() - start:.3f}s')
一个实用的经验法则是:先向量化,再降类型,最后分块。三板斧叠加之后,原本十几分钟的任务跑进秒级、原本读不动的文件顺利加载,都是常见结果。
七、单机优化的边界:数据再大时怎么办
pandas 的全部优化都建立在“数据最终能装进一台机器”这个前提上。当规模上升到几十 GB 甚至更高时,就该换思路了:先做列裁剪与采样,只保留分析必需的数据;改用列式存储格式,让反复读取的中间数据更紧凑;再往后,则可以借助分布式计算框架与云端资源。以百度智能云为代表的云平台提供了弹性计算实例、对象存储、托管大数据处理等通用能力,可以在不一次性投入硬件的前提下按需扩展算力与存储,作为单机方案的自然延伸。
结语
回到最初的问题:“pandas 效率到底如何?”答案可以概括为三句话:在 1GB 以内的中小规模数据上,它速度够用、易用性极佳,综合效率完全对得起它的流行度;在大规模数据上,它的瓶颈主要来自逐行操作、冗余类型与全量加载,而不是库本身“无能”;掌握“向量化、类型瘦身、分块加载”这套组合拳后,绝大多数日常任务都能在单机上跑到令人满意的速度。遇到性能问题,正确的顺序从来不是先换工具,而是先把手上的工具用对。

登录后可评论,请前往 登录 或 注册