找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2671

积分

0

好友

377

主题
发表于 3 小时前 | 查看: 0| 回复: 0

先讲个真事儿。前两天晚上十一点多,我正准备关电脑回家,我们组那个小李突然在工位后面喊一句:“哥,我这脚本是不是又卡死了?”一看,他那段 pandas 脚本跑了快二十分钟,CPU 一直 100%,结果 DataFrame 还没处理完,数据量也就几十万行。

一打开代码,我就笑了:典型的“for + loc”炼丹大法。

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

你可能也写过类似的,对吧?逻辑一点问题没有,就是慢得离谱。问题不是循环本身,而是 「循环里频繁用 loc / iloc」

为啥这么慢?简单说三点,别整太学术的:

第一,每次 df.loc[i, 'age'] 都要查一遍索引,相当于一直在问:“第 i 行你在哪?” 第二,df.loc[i, 'is_adult'] = ... 是一次一次的赋值,pandas 背后要做各种对齐、检查,很多看不见的活。 第三,你在 Python 的 for 里跑这些操作,等于让解释器在最慢的那层一行一行搬砖,完全没用到 pandas 本来擅长的“整列计算”。

所以一圈下来,你以为是 O(n),实际上所有隐形开销叠加上去,肉眼可见就慢。

那正常姿势应该怎么写?我当场就给小李改成这样:

df['is_adult'] = (df['age'] >= 18).astype(int)

一行,本地测了下,直接从几十秒变成几百毫秒级。这个就叫“向量化”:一次性对一整列算,而不是一行一行算。

再举个小李代码里的例子,他还写了这么一段拼全名的:

for i in range(len(df)):
    df.loc[i, 'full_name'] = df.loc[i, 'first_name'] + ' ' + df.loc[i, 'last_name']

改完:

df['full_name'] = df['first_name'] + ' ' + df['last_name']

pandas 内部会帮你把字符串一列一列拼好,比你 for 循环快太多了。

有时候你可能说,我这个逻辑有点复杂,没法直接用一两行布尔表达式搞定,那也别第一反应就上 for + loc。至少还可以这样折中一下,用 apply

def calc_level(row):
    if row['score'] >= 90:
        return 'A'
    elif row['score'] >= 80:
        return 'B'
    else:
        return 'C'

df['level'] = df.apply(calc_level, axis=1)

严格讲,apply(axis=1) 底层还是一行一行跑 Python 代码,但它至少不会在里面疯狂 .loc 回写,每一行会一次性取好整行数据传给你,少很多开销。真没办法的时候,用 apply 也比你手写 for + loc 要体面。

再狠一点,其实很多“看着复杂”的逻辑拆一拆也能变成向量化。比如按多个条件打标签,小李原来是这样:

for i in range(len(df)):
    if df.loc[i, 'age'] >= 18 and df.loc[i, 'city'] == 'Beijing':
        df.loc[i, 'tag'] = 'adult_bj'
    else:
        df.loc[i, 'tag'] = 'other'

我让他拆成两个布尔条件,用 np.where

import numpy as np

mask = (df['age'] >= 18) & (df['city'] == 'Beijing')
df['tag'] = np.where(mask, 'adult_bj', 'other')

既清晰,又快。很多业务里的“规则表”,本质就是一堆条件组合,用布尔向量撸一撸就干净了。

那是不是以后完全不能循环了?也没这么绝对。有几种情况,真要在 Python 里绕一圈,那也尽量别用 loc/iloc

比如你只是读,不写 DataFrame,可以用 itertuples()

for row in df.itertuples():
    # row 是个 namedtuple,访问字段用 row.age、row.city
    do_something(row)

itertuples() 会比 iterrows() 快一截,也比你 range(len(df)) 然后 loc 好得多。

又比如,你必须循环计算一个新列,但又想少点开销,可以先用普通 list 存结果,最后一次性塞回去,不要在循环里一行一行 loc 写:

values = []
for row in df.itertuples():
    if row.age >= 18:
        values.append(1)
    else:
        values.append(0)

df['is_adult'] = values

这种写法至少把“写回 DataFrame”这步从 n 次变成了一次。

还有一个细节顺便说下:如果你只是改单个标量,用 at / iat 会比 loc / iloc 轻量一点,比如:

df.at[10, 'age'] = 20 # 按标签
df.iat[5, 3] = 100 # 按位置

但说实话,真到了要在循环里一行一行调 at/iat 的程度,那整体思路八成还是可以改成向量化的,别把这个当成性能银弹。

回到那天晚上,小李看我把几段代码改成一两行向量化之后,脚本重新跑了一遍,还没去打个水就跑完了,他自己都愣住了。其实 pandas 本来就是为“整列、整块数据”设计的,你非要当成 list 去一行一行点 loc,就像拿挖掘机拧螺丝,工具没错,用法太折腾。

所以你以后只要记住一个小 checklist 就行了: 先想能不能用列运算、布尔过滤、wheregroupby 之类搞定; 实在不行再考虑 apply; 最后万不得已再写 Python 循环,但别在循环里疯狂 loc/iloc 改 DataFrame。

行了,先这样,等你哪天碰到一个“跑一晚上”的 pandas 脚本,第一件事就去找找那个藏在角落里的 for i in range(len(df)) 吧。

希望这个小案例能帮你避开数据处理中的常见性能陷阱。如果你想了解更多类似的技术技巧和深度解析,可以关注我们的 云栈社区,那里有更多开发者分享的实战经验与避坑指南。




上一篇:MySQL索引优化实战指南:分页、Join、Count与阿里规范解析
下一篇:geo_plotkit 模块升级:使用 add_province_names 函数为全国地图快速标注省名
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-26 17:27 , Processed in 0.264093 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表