☰
Pandas行迭代性能对比:iterrows、itertuples与items选型指南
2026/10/27 15:25:40 网站建设 项目流程

做数据分析这行,只要用过一段时间 Pandas,几乎都绕不开一个场景:手里攥着一个 DataFrame,想按行处理点东西。新手期最直觉的写法就是for index, row in df.iterrows(),代码跑起来也没报错,于是这个习惯就跟着走了好几年。直到某天处理一个几十万行的表,发现光遍历就卡了十几秒,才开始回头翻文档,才发现 Pandas 官方在iterrows()的说明里其实写得很直白——它慢,而且会做类型转换,能不用就别用。

这篇东西就是把我自己踩过的坑、做过的对比测试、以及最后沉淀下来的选型经验整理出来。核心围绕三个迭代方法:iterrows()、itertuples()、items()。它们看起来都是"遍历 DataFrame",但底层实现、性能表现、适用场景差别非常大,用错了轻则慢十倍,重则数据被悄悄改了类型你还不知道。适合已经会用 Pandas 做基础操作、但想搞清楚"到底该用哪个"的中级使用者,也适合刚接手别人代码、看到满屏iterrows想重构的开发者。下面从设计思路讲到实操对比,再到排查技巧,尽量把每个"为什么"都讲透。

1. 三种迭代方式的设计逻辑与选型考量

1.1 为什么 Pandas 要提供三种"遍历"入口

要理解这三个方法,得先明白 Pandas 的底层数据结构。DataFrame 本质上是按列存储的——每一列是一个独立的 NumPy 数组(或 ExtensionArray),列与列之间通过索引对齐。这个设计决定了 Pandas 最擅长的操作是列级向量化运算,而不是逐行扫描。你可以把它想象成一张 Excel 表,但底层其实是"一摞按列叠好的卡片",而不是"一行行排好的记录"。

那为什么还要提供行迭代的接口?因为现实里总有些逻辑没法向量化。比如要根据当前行的值去查另一张表、要调用一个只接受标量的外部函数、要做带状态的行间累积计算。这些场景下,你不得不把数据"拉成一行一行"来处理。Pandas 提供三个入口,本质上是在便利性和性能之间给出不同档位的选择:

  • iterrows():返回(index, Series),最符合直觉,但每行都要构造一个 Series 对象,开销最大,且会做 dtype 推断导致类型可能被"升格"。
  • itertuples():返回命名元组(namedtuple),每行是一个轻量元组,几乎不构造额外对象,速度快一个数量级。
  • items():按列迭代,返回(列名, Series),适合"逐列处理"而非"逐行处理"的场景。

我个人的理解是:iterrows()是给"写起来最顺手"准备的,itertuples()是给"性能敏感"准备的,items()则是被很多人忽略、但在列处理场景下非常趁手的工具。选型的核心判断标准只有一条——你到底是按行想问题,还是按列想问题。

1.2 一个容易被忽略的前提:能向量化就别迭代

在展开三个方法之前,必须先泼一盆冷水:绝大多数你以为"必须遍历"的需求,其实都能向量化。这是选型的第一道关卡,过不了这关,讨论用哪个迭代方法都是舍本逐末。

举个最常见的例子。假设要新增一列,值是另外两列之和。新手写法是遍历每一行相加,老手直接df['c'] = df['a'] + df['b']。后者底层走的是 NumPy 的 C 循环,前者是 Python 层的 for 循环加对象构造,差距在数据量大时是几十倍甚至上百倍。

再比如条件赋值。遍历里写if row['score'] > 60: ...,向量化写法是df.loc[df['score'] > 60, 'grade'] = 'pass'。字符串处理、日期提取、分组聚合,Pandas 都有对应的向量化或apply接口。

提示:判断能不能向量化的土办法——如果你的循环体里只用到"当前这一行"的数据,且不依赖上一行的结果、不调用外部有状态函数,那大概率能向量化。只有当你需要跨行状态、外部 IO、或者复杂分支逻辑时,才真正需要迭代。

我见过太多项目,性能瓶颈就卡在一个本可以向量化的iterrows循环上。所以下面的所有讨论,都建立在"确实需要迭代"这个前提之上。

1.3 三者的底层差异决定了性能天花板

把三者的返回结构摊开看,性能差异的根源一目了然。

iterrows()每次迭代做这几件事:从各列数组里取出第 i 个元素,组装成一个 Series,Series 还要带上原 DataFrame 的列名作为索引标签,同时根据取出的值推断 dtype。构造 Series 是个相对重的操作,涉及索引对象创建、类型推断、内存分配。数据量大时,这个开销会线性累积。

itertuples()走的是另一条路:它把每行的值打包成一个 namedtuple,字段名就是列名(非法标识符的列名会被重命名或跳过,这点后面细说)。namedtuple 是 Python 标准库里的轻量结构,创建成本极低,访问字段走的是属性查找,比 Series 的标签索引快得多。官方文档明确说它"通常比 iterrows 快得多",实测也确实如此。

items()则是完全不同的维度——它迭代的是列。每次返回列名和该列的 Series(注意这里是整列,不是单行)。它的开销主要在返回整列 Series 上,但因为列数通常远小于行数,总迭代次数少,所以整体开销可控。它适合的场景是"对每一列做统一处理",比如逐列统计缺失率、逐列做类型转换。

下面这张表是我自己整理的三者对照,建议收藏:

维度iterrows()itertuples()items()
迭代单位行行列
返回结构(index, Series)namedtuple(列名, Series)
单次开销高(构造 Series)低(构造元组)中(返回整列)
dtype 保持可能被升格保持原值类型保持列 dtype
相对速度慢(基准 1x)快(约 10-50x)视列数而定
典型场景调试、小数据生产环境行处理逐列处理

理解了这张表,后面的实操和避坑就有了依据。

2. 核心方法逐个拆解与实操要点

2.1 iterrows():最顺手,也最容易埋雷

iterrows()的签名很简单,返回一个生成器,每次产出(index, Series)。写起来确实舒服,row['列名']直接取值,可读性拉满。但它有两个必须知道的坑。

第一个坑是dtype 升格。这是最隐蔽的问题。DataFrame 里如果某一列是整数类型,但同一行里还有浮点列,iterrows()在构造 Series 时会把整行统一成能容纳所有值的类型,整数可能被转成浮点。我实测过一个例子:一列是int64的 ID,一列是float64的金额,遍历时row['id']拿到的居然是1001.0而不是1001。如果你的后续逻辑依赖整数做字典键或者字符串拼接,就会出问题。

import pandas as pd df = pd.DataFrame({'id': [1001, 1002], 'amount': [12.5, 30.0]}) for idx, row in df.iterrows(): print(type(row['id']), row['id']) # 输出: <class 'numpy.float64'> 1001.0 # 注意 id 从 int 变成了 float

第二个坑是性能。前面说过,每行构造 Series 的开销在大数据量下非常可观。我做过一个粗略测试,10 万行、5 列的 DataFrame,纯遍历不做任何操作,iterrows()大约要 1.5 到 2 秒,而itertuples()只要 0.05 秒左右,差距在 30 倍上下。这个倍数会随列数增加而进一步拉大,因为 Series 构造的成本和列数正相关。

那iterrows()还有没有使用价值?有。调试阶段、数据量小(几千行以内)、需要频繁按列名取值且列名不规范时,它依然是最省心的选择。我的习惯是:原型验证用iterrows()快速跑通逻辑,确认无误后再视性能需求迁移到itertuples()。

注意:如果确实要用iterrows()且关心类型,可以在取值时显式转换,比如int(row['id'])。但这属于打补丁,治标不治本,数据量大时还是建议换方法。

2.2 itertuples():生产环境的首选,但有两个细节要盯

itertuples()返回 namedtuple,默认第一个字段是Index(原索引),后面依次是各列。访问方式从row['列名']变成row.列名,属性访问比字典式查找快,这也是它性能好的原因之一。

for row in df.itertuples(): print(row.Index, row.id, row.amount)

它有两个参数值得关注。index=True是默认值,会带上索引列;如果不需要索引,设index=False能再省一点开销,返回的元组第一个字段直接就是第一列。name='Pandas'控制返回的 namedtuple 类型名,一般不用改。

第一个细节是列名合法性。namedtuple 的字段名必须是合法的 Python 标识符,不能以数字开头、不能含空格或特殊字符。如果 DataFrame 的列名是'user id'(带空格)或者'2023'(数字开头),itertuples()会把这些列重命名为_1、_2这样的位置名,或者直接跳过。这意味着你按row.user_id取值会报AttributeError。解决办法有两个:迭代前先把列名规范化(df.columns = df.columns.str.replace(' ', '_')),或者用位置索引row[1]取值。

第二个细节是它不做 dtype 升格。因为 namedtuple 里存的是各列原始的值,整数还是整数,浮点还是浮点。这一点比iterrows()靠谱得多,也是我推荐它在生产环境使用的核心理由之一。

# 列名不规范时的表现 df2 = pd.DataFrame({'user id': [1, 2], '2023': [10, 20]}) for row in df2.itertuples(): print(row) # 输出: Pandas(Index=0, _1=1, _2=10) # 注意列名被替换成了 _1, _2

实测下来,itertuples()在 10 万行数据上的遍历耗时稳定在几十毫秒级别,是三者中行迭代的最优解。唯一要付出的代价是可读性略降——row.列名不如row['列名']直观,且列名含特殊字符时要额外处理。

2.3 items():被低估的逐列处理利器

items()迭代的是列,每次返回(列名, Series)。很多人第一次看到它会疑惑:"这跟直接for col in df.columns有什么区别?"区别在于items()直接把整列数据也给你了,省去了一次df[col]的查找。

它的典型场景是逐列做统一处理。比如要统计每列的缺失率、每列的唯一值数量、或者对每列做类型检查。这类需求如果用行迭代去想,会非常别扭;用items()就顺理成章。

for col_name, col_data in df.items(): missing_rate = col_data.isna().mean() print(f"{col_name}: 缺失率 {missing_rate:.2%}")

这里有个性能上的认知要纠正:items()返回的 Series 是原列的视图还是副本,取决于具体操作。如果只是读取统计,不会触发复制;但如果对返回的 Series 做修改,不一定会写回原 DataFrame,这点和直接df[col] = ...的行为不同,容易踩坑。我的经验是:用items()做只读分析最安全,要做修改就老老实实回到列赋值。

另外,items()的迭代次数等于列数,通常几十到几百,远小于行数,所以它的总开销天然就低。在列数不多的宽表上,它甚至比任何行迭代都快——因为你根本不需要遍历那么多行。

2.4 三者选型的决策清单

讲完三个方法,给一个我实际工作中用的决策流程,照着走基本不会选错:

  1. 先问:这个逻辑能不能向量化?能,就别迭代,直接写向量化表达式。
  2. 不能向量化,且是按行处理:数据量小或调试阶段用iterrows();生产环境、数据量大用itertuples()。
  3. 不能向量化,且是按列处理:用items()。
  4. 需要跨行状态(比如累积、滑动窗口):优先考虑itertuples()配合外部变量,或者干脆用shift、cumsum等向量化替代。

这个清单看起来简单,但真正落地时,第 1 步就能筛掉八成以上的迭代需求。剩下的两成里,itertuples()又能覆盖大部分。iterrows()的实际使用比例,在我重构过的代码里通常不到 5%。

3. 完整实操:从慢代码到快代码的重构过程

3.1 场景设定与初始慢实现

光讲理论不够,拿一个真实感强的场景走一遍。假设有一张订单表,字段包括订单号、用户 ID、金额、下单时间。需求是:对每个用户,按时间顺序累计其消费金额,并标记出累计金额首次超过阈值的订单。

这个需求带跨行状态(累计值依赖前面的行),没法简单向量化,是迭代的合理使用场景。先看新手最常见的iterrows()写法:

import pandas as pd df = pd.DataFrame({ 'order_id': range(1, 6), 'user_id': [101, 101, 102, 101, 102], 'amount': [50.0, 80.0, 30.0, 120.0, 90.0], 'order_time': pd.to_datetime([ '2024-01-01', '2024-01-02', '2024-01-02', '2024-01-03', '2024-01-04' ]) }).sort_values(['user_id', 'order_time']).reset_index(drop=True) threshold = 100 cum = {} flags = [] for idx, row in df.iterrows(): uid = row['user_id'] cum[uid] = cum.get(uid, 0) + row['amount'] flags.append(cum[uid] > threshold) df['over_threshold'] = flags

这段代码逻辑没问题,跑小数据也对。但注意row['user_id']在这里因为同行有 float 列,可能被升格成 float,虽然本例中 user_id 恰好没受影响(因为 amount 是 float 但 user_id 是 int,Series 会统一成 float),实际取值时uid可能是101.0。用它做字典键,101和101.0在 Python 里哈希相等,所以本例侥幸没出错,但这是运气,不是设计。

3.2 迁移到 itertuples() 的改造

把上面的循环换成itertuples(),改动很小,但收益明显:

cum = {} flags = [] for row in df.itertuples(index=False): uid = row.user_id cum[uid] = cum.get(uid, 0) + row.amount flags.append(cum[uid] > threshold) df['over_threshold'] = flags

这里用了index=False,因为逻辑里不需要原索引。row.user_id拿到的是原始 int,row.amount是原始 float,类型干净。在 10 万行数据上,这段改造后的代码比iterrows()版本快 20 倍以上,而且结果完全一致。

改造时有个细节要注意:itertuples()的字段名来自列名,如果列名是user_id这种下划线命名,直接row.user_id就行;如果列名是userId驼峰式,也是合法的,row.userId可用;但如果是user-id带连字符,就会被重命名,得改用位置索引。所以改造前先print(df.columns.tolist())确认一下列名规范,能省不少调试时间。

3.3 进一步优化:能向量化的部分抽出来

上面的循环里,其实"累计"这一步可以用groupby加cumsum向量化,只有"首次超过阈值"的标记需要额外处理。进一步优化:

df['cum_amount'] = df.groupby('user_id')['amount'].cumsum() df['over_threshold'] = df['cum_amount'] > threshold

一行groupby().cumsum()就替代了整个手动累计循环,底层走的是高度优化的分组聚合,速度比 Python 循环快一到两个数量级。这就是我反复强调"先想向量化"的原因——很多看起来必须迭代的逻辑,拆解后总有一部分能交给 Pandas 的底层实现。

当然,不是所有跨行逻辑都能这么优雅地向量化。如果业务规则更复杂,比如"累计金额超过阈值后,后续订单打折计算",那可能还是得保留迭代。这时候itertuples()就是兜底方案。我的实践原则是:能用向量化解决的绝不迭代,必须迭代的优先 itertuples。

3.4 性能实测数据与解读

为了让你对差距有直观感受,我构造了三组不同规模的数据做对比测试(纯遍历,循环体只做一次加法,排除业务逻辑干扰):

数据规模iterrows()itertuples()items()(按列)
1 千行约 15 ms约 1 ms约 0.5 ms
1 万行约 150 ms约 8 ms约 0.6 ms
10 万行约 1.6 s约 60 ms约 0.7 ms

数据是量级参考,具体机器会有浮动,但趋势很稳定:iterrows()随行数线性增长且斜率大,itertuples()斜率小一个数量级以上,items()因为按列迭代,行数增加几乎不影响它的耗时(列数不变的前提下)。

这个表还揭示了一个反直觉的点:在宽表(列多行少)场景下,items()可能比任何行迭代都快。所以选型不能只看"行还是列",还要看数据的形状。行数远大于列数是常态,此时行迭代是主要矛盾;但如果遇到列数上千的宽表,逐列处理反而更划算。

4. 常见问题排查与避坑经验实录

4.1 类型悄悄变了:最坑的隐形 bug

前面提过iterrows()的 dtype 升格,这里展开讲排查方法。症状是:明明 DataFrame 里是整数,遍历时取出来变成了浮点,导致后续字符串拼接出现'1001.0'这种脏数据,或者字典键匹配失败。

排查思路:在循环体第一行打印type(row['目标列']),如果发现是numpy.float64而预期是numpy.int64,就是踩了这个坑。根治方法是换itertuples(),或者迭代前把该列单独取出来用zip遍历:

for uid, amt in zip(df['user_id'], df['amount']): # uid 保持原始 int 类型 ...

zip遍历多列是我个人很喜欢的轻量方案,尤其适合只需要两三列的场景,比itertuples()还直接,且完全没有类型问题。缺点是列多了写起来啰嗦,且拿不到索引。

4.2 迭代时修改 DataFrame 引发的诡异行为

另一个高频坑是:在迭代过程中修改原 DataFrame。比如遍历时给某列赋值、增删行。iterrows()和itertuples()在迭代开始时就基于当时的 DataFrame 状态生成迭代器,迭代中修改原表,轻则修改不生效,重则触发SettingWithCopyWarning或者行为不可预期。

正确做法是:先收集结果,迭代结束后统一赋值。就像 3.1 节里用flags列表收集、循环外再df['over_threshold'] = flags。这个模式几乎适用于所有"迭代中要写回"的场景,既避免了修改冲突,又让逻辑更清晰。

注意:如果确实需要在迭代中读到自己刚写的值,那说明逻辑本身有状态依赖,应该重新设计,而不是硬在循环里改表。

4.3 列名不规范导致的 AttributeError

itertuples()遇到非法列名会重命名,症状是row.原列名报AttributeError。排查方法:先print(df.itertuples().__next__())看一眼实际字段名。解决要么规范化列名,要么用位置索引。

我整理了一个常见问题速查表,遇到问题先对号入座:

症状可能原因解决方向
取出的整数变成浮点iterrows 的 dtype 升格换 itertuples 或 zip
row.列名 报 AttributeError列名含空格/特殊字符/数字开头规范化列名或用位置索引
迭代中赋值不生效迭代器基于旧状态循环外统一赋值
遍历特别慢用了 iterrows 处理大数据换 itertuples 或向量化
结果和预期差一位index=True 时首字段是索引设 index=False 或跳过首字段

4.4 几个提升可维护性的小技巧

最后分享几个实操中沉淀的习惯。第一,迭代前先确认数据形状,df.shape打印一下,行数超过十万就要警惕性能,提前想好是否必须迭代。第二,把迭代逻辑封装成函数,输入是行或元组,输出是处理结果,这样既方便单测,也方便日后从itertuples换成别的实现而不动主流程。第三,给循环加进度提示,数据量大时用tqdm包一层,能直观看到卡在哪,排查性能问题时有奇效。

from tqdm import tqdm results = [] for row in tqdm(df.itertuples(index=False), total=len(df)): results.append(process_row(row))

tqdm的开销很小,但在处理几十万行时,那个进度条能帮你判断是"整体慢"还是"某几行特别慢",对定位问题很有帮助。这个技巧我在多个项目里用过,尤其是处理外部接口调用这种单行耗时不均的场景,效果立竿见影。

我个人在实际操作中的体会是,这三个方法没有绝对的好坏,只有合不合适。真正拉开水平差距的,不是记住哪个方法快,而是养成"先问能不能向量化、再问按行还是按列、最后才选具体方法"的思考顺序。这个顺序一旦形成,代码的性能和可维护性会同时上一个台阶。至于iterrows(),我现在基本只在写一次性脚本、数据量几百行的时候用它图个顺手,正式代码里见到它,第一反应就是看看能不能换掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询