1. 性能瓶颈诊断

说实话,我见过太多团队在特征生成上栽跟头。代码写完了,一跑就是几个小时。大家第一反应往往是「加机器」、「上Spark」。但很多时候,问题根本不在资源上。

你想想看,如果一段代码里有个死循环,或者某个函数被调了上百万次,加再多机器也没用。所以第一步,得先找到「慢在哪」。

1.1 特征生成的常见性能陷阱

我个人习惯把性能问题分成三类。先看个表格,心里有个谱:

类型 典型表现 常见原因
计算密集 CPU 跑满,单核 100% 复杂数学运算、循环内重复计算
I/O 密集 磁盘或网络等待时间长 频繁读写文件、数据库查询过多
内存密集 内存占用高,频繁 GC 大 DataFrame 复制、中间结果未释放

我在项目中遇到过最坑的一次:一个特征生成脚本跑了 6 小时。用工具一看,90% 的时间花在 Pandas 的 apply 函数上。说白了,就是逐行调用了 Python 原生函数,每次调用都有巨大的开销。

⚠️ 避坑指南: 我曾经以为「能跑就行」,结果上线前发现特征生成要 8 小时。记住:性能问题越早发现,改造成本越低。

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 两张工具怎么配合?

我一般这么用:

  1. 先用 cProfile 扫全局,找出最耗时的 3-5 个函数。
  2. 再用 line_profiler 盯细节,对热点函数逐行分析。
  3. 最后结合代码逻辑,判断是算法问题还是实现问题。

举个例子。有一次我发现一个特征聚合函数特别慢。cProfile 告诉我 groupby_agg 占了 70% 的时间。line_profiler 进一步定位到 lambda x: x.rolling(5).mean() 这行。嗯,问题找到了——滚动窗口计算没有向量化。

1.5 本章知识体系

下面这张图总结了性能诊断的核心流程:

特征生成性能诊断流程 特征生成性能瓶颈 计算密集 I/O 密集 内存密集 cProfile 全局扫描 line_profiler 逐行分析 结合代码逻辑判断 热点函数列表 每行耗时占比 性能根因定位 针对性优化:算法改进 / 向量化 / 缓存 / 并行 诊断不是目的,找到根因才能对症下药
💡 我的建议: 别一上来就优化。先跑一轮诊断,把数据拿到手。我见过太多人凭直觉改代码,结果越改越慢。数据不会骗人。

好了,这一章的核心就这些。记住:先诊断,后优化。cProfile 和 line_profiler 就是你的听诊器和 CT 机。下一轮遇到特征生成慢的问题,别急着加机器,先跑一遍分析再说。