Python技术迷

不要再在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