不要再在pandas循环中使用 loc/iloc 了!
前两天晚上快十一点,我在公司楼下奶茶店蹲着改报表,电脑风扇呼呼响,SQL 跑得飞快,结果 Python 那段死活跑不完。 一看代码,熟悉的味道来了:
for i in range(len(df)):
if df.loc[i, 'age'] > 18:
df.loc[i, 'is_adult'] = 1
else:
df.loc[i, 'is_adult'] = 0
是不是很眼熟?很多人第一次用 pandas 基本都这么写,包括我。然后表一大,直接卡到你怀疑人生。
为啥循环里用 loc/iloc 这么慢?
你先有个直觉就行:df.loc[i, 'col'] 不是简单的“取一个数组元素”,它会经历一堆逻辑: 索引对齐、类型检查、复制、对齐返回……你在循环里每转一圈,让 pandas 走一遍这一整套流程,相当于让快递小哥一次次跑整条街,只为了给你送一张纸。
10 条数据没感觉,10 万、100 万就开始原地爆炸了。
那正确姿势是啥:能不循环就不循环
上面那个按年龄打标签,用 pandas 正常写法其实就三行:
import numpy as np# 新列先给个默认值
df['is_adult'] = 0
# 条件索引 + 一次性赋值
df.loc[df['age'] >= 18, 'is_adult'] = 1
或者更干脆一点:
df['is_adult'] = np.where(df['age'] >= 18, 1, 0)
这里有几个小点你记一下(真的实战里超常用):
条件判断,直接拿整列比较: df['age'] >= 18得到的是一个布尔 Series用这个布尔条件去 loc一次性选中所有符合条件的行统一赋值,而不是一行一行改
这就是所谓“向量化”操作:一口气对整列、整块数据下手,不要在人为地去 for。
再举个稍微复杂点的,多个字段一起算
比如我们电商那边经常要算“含税金额 + 是否大额订单”这种:
天真的写法可能是:
for i in range(len(df)):
price = df.loc[i, 'price']
tax = df.loc[i, 'tax_rate']
total = price * (1 + tax)
df.loc[i, 'total_price'] = total
df.loc[i, 'is_big'] = 1if total > 1000else0
换成 pandas 思路,其实就几行:
df['total_price'] = df['price'] * (1 + df['tax_rate'])
df['is_big'] = (df['total_price'] > 1000).astype(int)
注意一个小技巧: 布尔值转 0/1,直接 .astype(int),不用再写什么三元表达式了。
那有没有“被迫”要循环的场景?
有,业务总能整出花活来,比如:
每一行要调一个外部接口 每一行要跑一个特别复杂的规则树 某些操作真的是“上一个结果影响下一个”
这种时候你真要循环,行,那就尽量减少伤害,别在循环里疯狂 loc/iloc 了。
至少改成这样:
for i, row in df.iterrows():
# row 是一行的数据
score = complex_func(row['a'], row['b'])
df.at[i, 'score'] = score
或者用 itertuples 更快一点:
for row in df.itertuples():
score = complex_func(row.a, row.b)
df.at[row.Index, 'score'] = score
这里有两个点:
itertuples()比iterrows()快一点,因为返回的是类似命名元组的东西写回去用 .at/.iat,比loc/iloc要轻量一些
但说实话,只要你看到自己写了 for i in range(len(df)):,基本就该心里一紧:是不是写得有点问题。
很多人忽略的“大杀器”:groupby 和 transform
这个是真实项目里救过命的。
举个:想给每个用户算“该用户的订单金额占他所有订单总额的比例”。
很多人第一反应是双层循环、或者外面先 group 再里面一堆 loc。其实可以这么玩:
user_sum = df.groupby('user_id')['amount'].transform('sum')
df['ratio'] = df['amount'] / user_sum
transform('sum') 的意思是: 对每个 user_id 求和,然后把这个“和”再 广播回原来的每一行,索引对齐帮你搞定了。你就直接用来算比例就行。
完全没有显式循环,也没有在循环里瞎 loc,性能直接上一个档次。
为什么我这么执着让你改掉这个习惯?
因为我真被它坑过太多次了。 有一次线上的一个报告脚本,逻辑非常简单,就是各种 if-else 打标记,但数据量有个几百万行,结果一跑就是十几分钟,运维以为机器又要撑不住了。
最后一看,核心逻辑居然就是:
for i in range(len(df)):
if df.loc[i, 'xxx'] == 'A':
df.loc[i, 'tag'] = '...'
改完写法,换成纯向量化 + 一两个 groupby,从 10 分钟掉到十几秒,业务那边以为我换了服务器,其实就动了几行代码。
你可以先从一个小习惯开始改: 只要你的 pandas 代码里出现了这种模式——
for ...
df.loc[...]
就给自己敲个警钟:能不能把这段挪成“对一整列/一整块的数据操作”。 真不行,再考虑循环 + itertuples + .at 这种“减伤版”。
行了,我先去倒杯水,你可以翻翻自己项目里老代码,看看哪段循环里还在用 loc/iloc,改掉一个,性能就赚一笔。
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB