资讯动态

Pandas迭代性能优化:iterrows、itertuples与items选型指南

发布时间:2026/10/27 6:29:23 来源:尧图企业网站定制
做数据处理的人几乎都绕不开一个场景手里攥着一个几十万行的 DataFrame需要逐行做点带条件的加工比如根据某几列的组合值去查一张映射表、拼一段文本、或者调用一个不方便向量化的外部函数。这时候脑子里蹦出来的第一个念头往往是for index, row in df.iterrows():代码写起来顺手读起来也直观。但我见过太多项目这段循环跑起来之后原本几秒钟能出结果的任务硬生生拖到了几分钟甚至十几分钟机器风扇呼呼转人只能干等着。问题不在于迭代这件事本身有错而在于很多人默认了iterrows()是唯一选项甚至以为它和itertuples()、items()只是写法上的口味差异。实际上这三者在底层的数据组织方式、内存开销、类型保持能力上差别相当大选错了轻则慢一个数量级重则把整数悄悄变成浮点数、把日期变成字符串埋下一堆难以察觉的脏数据隐患。这篇内容就围绕 Pandas 里这三种最常用的迭代方式展开把它们的机制、适用边界、实测差异和踩坑经验一次讲透适合已经会用 Pandas 做基本增删改查、但还没系统梳理过迭代性能的读者也适合那些被循环太慢困扰、想搞清楚到底该用哪个方法的人。1. 为什么逐行处理在 Pandas 里天然是个敏感话题1.1 向量化才是 Pandas 的主场迭代是退而求其次要理解三种迭代方法的取舍得先接受一个前提Pandas 的设计哲学是列式存储 向量化运算。它的底层大量依赖 NumPy 的连续内存块和 C 语言实现的循环当你写df[a] df[b]时真正干活的是编译好的底层代码Python 解释器只负责发号施令。而一旦你写出for ... in df.iterrows()控制权就交回给了 Python 解释器每一行都要经历一次对象创建、一次类型装箱、一次函数调用原本在 C 层能一口气跑完的事情被拆成了成千上万次解释器往返。我用一个很朴素的类比来说明向量化像是让一辆卡车一次性把货拉走迭代则像是让卡车每装一箱就往返一趟。货少的时候看不出差别货一多往返的路费解释器开销就彻底压过了运输本身。所以从业者的第一反应应该是能用向量化、apply、map、np.where解决的就别上显式循环。迭代方法的存在意义是留给那些确实无法向量化的场景——比如要调用一个只接受标量参数的第三方库函数、要按行做带副作用的写操作、或者逻辑分支复杂到用向量化表达反而不可读。1.2 三种方法各自的身份定位在动手比较之前先把三者的定位摆清楚后面所有的性能和行为差异都能从这里推导出来。方法每次迭代产出底层数据形态典型用途iterrows()(index, Series)元组每行现造一个 Series需要按列名访问、逻辑简单的小数据itertuples()命名元组namedtuple复用行数据、轻量结构追求速度、按属性或位置访问items()(列名, Series)元组按列迭代整列一个 Series需要逐列处理、列级聚合iterrows()返回的每一行是一个独立的Series索引是列名。这个 Series 有个致命特点它会把整行的数据类型统一提升到能容纳所有值的公共类型。一行里既有整数又有浮点数整数就被迫变成浮点一行里混了字符串和数字整行可能退化成object。这就是很多人迭代完发现我的 ID 怎么变成 1001.0 了的根源。itertuples()返回的是namedtuple默认第一个字段是索引可用indexFalse关掉后面按列顺序排列。它不构造 Series直接把底层数组的值塞进一个轻量元组所以速度快、内存省而且类型保持得更好——整数还是整数不会因为同行的浮点数被牵连。代价是不能用row[列名]这种字典式访问得用row.列名或row[1]。items()容易被误解它不是逐行而是逐列。每次产出一个(列名, 整列Series)。它的适用场景和前两者根本不同当你需要对每一列单独做统计、类型转换、或者把每列喂给不同的处理函数时items()才是对的工具。拿它去逐行处理属于用错了方向。2. iterrows 的真实开销一次迭代到底发生了什么2.1 拆解一次 iterrows 的完整链路很多人对iterrows()慢的认知停留在听说它慢但说不清慢在哪。我把一次迭代拆开看Pandas 内部会遍历底层数据块对每一行新建一个 Series 对象把该行各列的值逐个取出、装箱成 Python 对象、再塞进这个 Series 的底层数组。也就是说10 万行的表就要造 10 万个 Series每个 Series 都有自己的索引对象、数据缓冲区、元数据。这还没算你在循环体里访问row[某列]时Series 又要做一次索引查找。这里有个容易被忽略的细节iterrows()返回的 Series 索引是原 DataFrame 的列名但它的dtype是整行统一的。我做过一个对照实验构造一张包含idint64、pricefloat64、nameobject三列的表迭代时打印row.dtype结果是object——因为name列是字符串整行被迫降级成 object。这时候你再去row[id]拿到的可能已经不是原来的整数语义了。这种隐式的类型污染在数据量小的时候不容易发现等下游做数值比较或入库时才会突然报错。2.2 一个能直接复现的类型陷阱我建议每个用iterrows()的人都亲手跑一遍下面这段代码印象会比看十篇文章都深import pandas as pd df pd.DataFrame({ id: [1001, 1002, 1003], score: [88.5, 92.0, 79.5], tag: [a, b, c], }) for idx, row in df.iterrows(): print(idx, row[id], type(row[id]), row.dtype)跑完你会看到row[id]打印出来是1001但type是float64对应的 Pythonfloat而row.dtype是object。如果这张表里没有tag这列字符串只有 int 和 float那row.dtype会变成float64id依然是浮点。只要一行里存在浮点列整数列在 iterrows 里就保不住整数身份。这个行为不是 bug是 Series 同质化存储的必然结果但它足以让一个依赖整数主键的下游流程崩掉。提示如果你的循环体里需要拿原始整数做字典键、做精确相等判断、或者拼 SQL 主键iterrows()基本可以直接排除换itertuples()。2.3 iterrows 并非一无是处说了这么多缺点也得给它一个公道。iterrows()的可读性确实是三者里最好的row[列名]这种写法对刚接触 Pandas 的人最友好代码意图一目了然。在数据量只有几千行、逻辑分支又特别复杂比如要根据五六个列的组合走不同分支、每个分支还要调不同的外部函数的场景下用iterrows()换来的可维护性往往比那点性能损失更值。我个人的判断线是行数在万级以内、且逻辑无法优雅向量化时iterrows 可以用一旦上了十万级就得认真考虑换 itertuples 或者重构逻辑。3. itertuples 凭什么快命名元组的轻量哲学3.1 省掉的是造 Series这笔大头开销itertuples()快的核心原因就一句话它不造 Series。它把每一行的值直接打包成一个namedtuple而 namedtuple 是 Python 标准库里极其轻量的结构本质是一个字段名固定的元组创建成本远低于 Series。没有索引对象、没有数据缓冲区、没有 dtype 推断就是几个值按顺序排好。我做过一个粗略的实测同一台机器、同一份数据、同样的循环体只做累加10 万行三列的表方法耗时相对值内存峰值感受iterrows()约 1.0基准高Series 频繁创建回收itertuples()约 0.15 ~ 0.25明显更低items()逐列视列数而定列少时很快中等具体倍数会随列数、数据类型、机器状态浮动但itertuples 比 iterrows 快数倍这个结论是稳定的。列越多iterrows 造 Series 的成本越高差距越明显。3.2 属性访问与位置访问的取舍itertuples()默认返回的 namedtuple 第一个字段叫Index就是行索引后面按列顺序。访问方式有两种for row in df.itertuples(): # 属性访问可读性好 print(row.Index, row.id, row.score) for row in df.itertuples(indexFalse): # 关掉索引列按位置访问 print(row[0], row[1], row[2])这里有个实操细节值得说列名如果包含空格、特殊字符、或者以数字开头属性访问会失效因为 namedtuple 的字段名必须是合法标识符。遇到这种列名要么提前df.rename()改成合法名要么老老实实用位置访问row[i]。我踩过一次坑某张表的列名是订单 金额中间带空格row.订单 金额直接语法错误排查了半天才反应过来是命名元组的字段名限制。另外itertuples()有个name参数可以给返回的 namedtuple 起个类名默认是Pandas。这个参数平时用不上但在调试时打印type(row)看到Pandas会觉得莫名其妙知道有这么回事就行。3.3 类型保持整数终于是整数了回到前面那个类型陷阱同样的表用itertuples()迭代row.id拿到的就是 Pythonintrow.score是float各归各位不会互相污染。原因很简单namedtuple 里每个位置存的是从底层数组直接取出的标量没有经过 Series 的同质化过程。这一点对数据准确性要求高的场景比如金融金额、库存数量、主键 ID非常关键。不过要注意itertuples()对某些扩展类型比如带时区的时间戳、可空整数Int64的处理和iterrows()可能略有差异遇到这类列建议先小样本验证一下取出的值是否符合预期别直接上全量。4. items 被误解的角色它是逐列不是逐行4.1 逐列迭代的正确打开方式items()的签名是DataFrame.items()迭代产出(列名, Series)其中 Series 的索引是行索引。它天生就是为列级操作设计的。典型场景比如对每一列做独立的缺失值统计、对每一列按不同规则做类型转换、把每一列分别喂给不同的可视化函数。for col_name, col_series in df.items(): print(col_name, col_series.dtype, col_series.isna().sum())这段代码一次性把每列的类型和缺失数打出来比df.dtypes加df.isna().sum()两次遍历更紧凑逻辑也更集中。当你需要对每一列做点不一样的事时items()的表达力比先拿列名列表再df[col]更自然。4.2 拿 items 逐行处理是方向性错误我见过有人写for name, series in df.items(): for i in series: ...来模拟逐行遍历这属于绕远路。items()每次给你的是整列你要逐行还得在列内部再套一层循环等于把按列组织的数据强行掰成按行消费中间多了一层无谓的拆解。真需要逐行直接用itertuples()或iterrows()别用items()硬凑。反过来说如果你发现自己的循环体里对每一列做的操作高度一致比如都是series.fillna(0)那连items()都不必用直接df.fillna(0)向量化搞定。items()的价值恰恰在于每列处理逻辑不同的时候。4.3 列级聚合时 items 与向量化的边界有个中间地带值得单独提当你需要对每列做聚合但聚合函数是自定义的、无法直接用df.agg()表达时items()是个不错的载体。比如每列要按不同阈值做分箱统计thresholds {score: 90, price: 100} result {} for col, series in df.items(): if col in thresholds: result[col] (series thresholds[col]).sum()这种列名驱动不同参数的逻辑用items()写出来最直白。但如果阈值对所有列统一那就该用向量化比较(df threshold).sum()一行顶一个循环。5. 选型决策把方法匹配到真实场景5.1 一张决策表帮你快速定位与其记一堆规则不如把判断压缩成几个问题按顺序问自己你的处境推荐方法理由逻辑能用向量化/apply 表达都不用直接向量化快一个数量级必须逐行、行数万级以内、逻辑复杂iterrows()可读性优先性能可接受必须逐行、行数大、或需保持整数类型itertuples()快数倍类型不污染需要按列处理、每列逻辑不同items()逐列语义天然匹配需要按行但列名不合法itertuples(indexFalse) 位置访问绕开字段名限制这张表不是死规矩但能覆盖八成日常决策。真正需要动脑的是能不能向量化这一关很多人跳过这关直接进循环才是性能问题的源头。5.2 一个真实的重构案例假设有个需求给一张订单表每行打标签规则是金额大于 500 且状态为已完成标优质金额大于 500 但状态未完成标待跟进其余标普通。新手写法labels [] for idx, row in df.iterrows(): if row[amount] 500 and row[status] done: labels.append(优质) elif row[amount] 500: labels.append(待跟进) else: labels.append(普通) df[label] labels这段代码在 5 万行上大概要跑一两秒逻辑清楚但没必要。向量化版本import numpy as np df[label] np.select( [ (df[amount] 500) (df[status] done), (df[amount] 500), ], [优质, 待跟进], default普通, )同样的数据向量化版本通常在几十毫秒量级。这个案例的意义不是说循环不能用而是提醒在跳进循环之前先花三十秒想想有没有np.select、np.where、map这类工具能一把梭。想不出来再迭代这时候再在iterrows和itertuples之间做选择。5.3 什么时候迭代反而是正确答案有些场景向量化真的无能为力比如循环体里要调用一个只接受单个字符串、内部还要发网络请求或读写文件的函数。这种带副作用、带外部依赖的逻辑天生就是逐行的。此时选itertuples()拿数据、在循环体里做副作用操作是干净的做法。我一般会配合tqdm加个进度条几十万行跑起来心里有数from tqdm import tqdm for row in tqdm(df.itertuples(indexFalse), totallen(df)): process(row.id, row.payload)注意totallen(df)要显式传否则 tqdm 拿不到总数进度条只会显示已处理数量而没有百分比。6. 迭代之外的性能护栏与常见坑6.1 循环里改原表SettingWithCopy 与性能双杀一个高频错误是在迭代过程中直接修改原 DataFramefor idx, row in df.iterrows(): df.loc[idx, new_col] row[a] row[b]这段代码有两个问题。第一每次df.loc赋值都是一次独立的索引定位和写入几十万行下来开销巨大第二如果df是某个切片的结果还可能触发SettingWithCopyWarning甚至改了个寂寞改的是副本。正确做法是先在循环外把结果收集到一个列表循环结束后一次性赋值results [] for row in df.itertuples(indexFalse): results.append(row.a row.b) df[new_col] results列表追加是 O(1) 摊还最后一次性赋值走的是向量化路径整体快得多也避开了切片赋值的坑。6.2 迭代中删除行是另一个雷区在iterrows()循环里df.drop(idx)是经典反模式边迭代边改结构轻则索引错乱重则行为未定义。需要按条件删行正确姿势是先算出要删的索引集合循环结束后统一df.drop(indexto_drop)或者干脆用布尔索引df df[~condition]一步到位。迭代只负责判断不负责修改结构这条边界守住了能省掉大量诡异 bug。6.3 大数据下的内存与分块思路当表大到几十万上百万行即便用itertuples()一次性把整表读进内存再迭代也可能吃紧。这时候可以考虑分块读取如果数据源支持或者用np.array_split把表切成若干块逐块迭代处理再合并结果。分块的好处是内存峰值可控坏处是要自己管理块间状态比如累加器、去重集合。我一般会在块大小上做个权衡块太小循环调度开销占比高块太大内存优势不明显。经验值是从几万行一块起步根据实际内存占用再调。6.4 别忘了先做类型优化迭代慢有时候不全是方法选错而是表本身的 dtype 太胖。object 类型的字符串列在内存里是一堆指针加独立对象迭代时取值也慢。如果某列其实是低基数类别转成category能显著降内存能用int32就别用int64。这些优化在迭代前做一遍往往比换迭代方法带来的收益还大。我习惯在拿到新表时先跑一遍df.info(memory_usagedeep)看看哪列在偷偷吃内存再决定要不要做类型收敛。7. 我个人的几条实操心得第一先问能不能不迭代这是所有优化的起点。我现在的习惯是只要脑子里冒出for ... in df.的念头就先停三秒想想np.where、np.select、map、apply、groupby.transform里有没有能替代的。十次里有六七次能找到向量化方案剩下三四次才真正需要迭代。第二默认用itertuples()除非可读性真的扛不住。它快、类型干净、内存友好唯一的门槛是属性访问对列名有要求。列名规范的表我基本无脑选它列名脏的表先 rename 再迭代比在循环里忍受row[带空格的列]更省心。第三iterrows()留给小数据和复杂分支。几千行、逻辑绕、需要频繁按列名取值的场景它的可读性优势是实打实的没必要为了那点性能把代码写得难维护。性能优化要看绝对收益几千行的循环快慢差异可能就几十毫秒不值得牺牲可读性。第四items()只在逐列语义成立时用。判断标准很简单你的循环体是不是对每一列做一件独立的事。是就用它不是就别硬套。第五迭代里绝不改结构只收集结果。这条守住了能避开一大半的诡异 bug 和性能陷阱。收集到列表、循环外一次性赋值是我这些年最稳定的写法。最后分享一个小技巧调试迭代逻辑时先df.head(20).itertuples()跑一遍把每行的处理结果打印出来核对确认逻辑无误再上全量。全量跑之前用tqdm包一层几十万行跑起来至少知道进度不至于盯着黑屏干等。这些细节看着琐碎但真到了生产环境省下的排查时间都是实打实的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑