1. 性能瓶颈诊断:特征生成慢在哪?
说实话,我见过太多团队在特征生成上栽跟头。代码写完了,一跑就是几个小时。大家第一反应往往是「加机器」、「上Spark」。但很多时候,问题根本不在资源上。
你想想看,如果一段代码里有个死循环,或者某个函数被调了上百万次,加再多机器也没用。所以第一步,得先找到「慢在哪」。
1.1 特征生成的常见性能陷阱
我个人习惯把性能问题分成三类。先看个表格,心里有个谱:
| 类型 | 典型表现 | 常见原因 |
|---|---|---|
| 计算密集 | CPU 跑满,单核 100% | 复杂数学运算、循环内重复计算 |
| I/O 密集 | 磁盘或网络等待时间长 | 频繁读写文件、数据库查询过多 |
| 内存密集 | 内存占用高,频繁 GC | 大 DataFrame 复制、中间结果未释放 |
我在项目中遇到过最坑的一次:一个特征生成脚本跑了 6 小时。用工具一看,90% 的时间花在 Pandas 的 apply 函数上。说白了,就是逐行调用了 Python 原生函数,每次调用都有巨大的开销。
1.2 用 cProfile 快速定位热点
cProfile 是 Python 自带的性能分析器。它不挑环境,装上就能用。我一般用它做第一轮扫描,看看全局的耗时分布。
用法很简单,两种方式:
方式一:命令行直接跑
python -m cProfile -o output.prof my_feature_gen.py
方式二:代码里嵌入
import cProfile
profiler = cProfile.Profile()
profiler.enable()
# 你的特征生成逻辑
generate_features(df)
profiler.disable()
profiler.dump_stats('feature_gen.prof')
跑完之后,你会得到一个 .prof 文件。怎么读?我推荐用 pstats 模块:
import pstats
p = pstats.Stats('feature_gen.prof')
p.sort_stats('cumtime').print_stats(20) # 看累计耗时前20
输出长这样:
ncalls tottime percall cumtime percall filename:lineno(function)
5000 0.002 0.000 45.321 0.009 feature_gen.py:42(compute_rolling)
1 0.001 0.001 30.123 30.123 feature_gen.py:10(generate_all)
10000 0.015 0.000 20.456 0.002 feature_gen.py:78(apply_transform)
看到没?compute_rolling 函数只被调了 5000 次,但累计耗时 45 秒。这就是热点。
cumtime(累计时间),再看 ncalls(调用次数)。如果调用次数少但累计时间长,说明函数本身慢;如果调用次数多,说明循环结构有问题。
1.3 用 line_profiler 逐行分析
cProfile 能告诉你哪个函数慢,但说不清「函数里哪一行慢」。这时候就得请出 line_profiler 了。
安装:
pip install line_profiler
使用方式是在目标函数上加 @profile 装饰器:
@profile
def compute_rolling(df, window=10):
result = []
for i in range(len(df)):
# 这行可能很慢
window_data = df.iloc[max(0, i-window):i+1]
# 这行也可能很慢
result.append(window_data.mean())
return result
然后运行:
kernprof -l -v my_feature_gen.py
输出会精确到每一行:
Line # Hits Time Per Hit % Time Line Contents
==============================================================
5 @profile
6 def compute_rolling(df, window=10):
7 1 2.0 2.0 0.0 result = []
8 5000 1500.0 0.3 0.1 for i in range(len(df)):
9 5000 450000.0 90.0 45.0 window_data = df.iloc[max(0, i-window):i+1]
10 5000 550000.0 110.0 55.0 result.append(window_data.mean())
11 1 1.0 1.0 0.0 return result
看到了吗?第 9 行和第 10 行占了 99% 的时间。每次循环都在做 iloc 切片和 mean 计算,这就是性能杀手。
🔍 关键发现: 这个例子中,iloc 每次都会复制数据,导致 O(n²) 的时间复杂度。优化方向应该是用滑动窗口算法,把复杂度降到 O(n)。
1.4 两张工具怎么配合?
我一般这么用:
- 先用 cProfile 扫全局,找出最耗时的 3-5 个函数。
- 再用 line_profiler 盯细节,对热点函数逐行分析。
- 最后结合代码逻辑,判断是算法问题还是实现问题。
举个例子。有一次我发现一个特征聚合函数特别慢。cProfile 告诉我 groupby_agg 占了 70% 的时间。line_profiler 进一步定位到 lambda x: x.rolling(5).mean() 这行。嗯,问题找到了——滚动窗口计算没有向量化。
1.5 本章知识体系
下面这张图总结了性能诊断的核心流程:
好了,这一章的核心就这些。记住:先诊断,后优化。cProfile 和 line_profiler 就是你的听诊器和 CT 机。下一轮遇到特征生成慢的问题,别急着加机器,先跑一遍分析再说。