2. 向量化操作

说实话,我见过太多数据工程师在特征生成时,写出一堆嵌套的for循环。代码跑起来慢得像蜗牛爬,自己还觉得挺正常。其实,这里有个性能提升的捷径——向量化操作。

我刚开始做特征工程那会儿,也踩过这个坑。有一次处理500万行用户行为数据,用for循环算一个简单的转化率特征,跑了整整40分钟。后来改成向量化写法,15秒搞定。嗯,从那以后我就再也没写过纯Python循环来处理数据了。

核心原则:能用NumPy/Pandas向量化操作,就别用for循环。向量化操作底层调用C语言实现,速度比Python循环快几十到几百倍。

为什么向量化这么快?

Python的for循环每迭代一次,都要做类型检查、对象创建、引用计数更新。这些开销在数据量小的时候不明显,但到了百万级、千万级数据,就成了性能杀手。

NumPy和Pandas的向量化操作,说白了就是把循环下沉到C层面执行。数据在内存中连续存储,CPU缓存命中率高,还能利用SIMD指令集做并行计算。你想想看,这差距能不大吗?

操作方式 执行层级 100万次操作耗时(参考) 内存效率
Python for循环 Python解释器 ~3-5秒 低(频繁创建临时对象)
列表推导式 Python解释器(优化) ~1-2秒 中
NumPy向量化 C语言层 ~0.01-0.05秒 高(连续内存块)
Pandas向量化 C语言层(基于NumPy) ~0.02-0.1秒 高

实战:从for循环到向量化

我拿一个实际场景来说。假设我们要生成一个特征:用户购买金额的对数变换。原始数据长这样:

import numpy as np
import pandas as pd
import time

# 模拟100万条交易数据
n = 1_000_000
df = pd.DataFrame({
    'user_id': np.random.randint(1, 10000, n),
    'amount': np.random.exponential(500, n) + 10  # 避免0值
})

# ❌ 错误示范:for循环
start = time.time()
log_amount_for = []
for i in range(len(df)):
    log_amount_for.append(np.log(df['amount'].iloc[i]))
df['log_amount_for'] = log_amount_for
print(f"for循环耗时: {time.time() - start:.2f}秒")

# ✅ 正确示范:向量化
start = time.time()
df['log_amount_vec'] = np.log(df['amount'])
print(f"向量化耗时: {time.time() - start:.2f}秒")

我在项目中实测过,for循环版本跑了4.2秒,向量化版本0.03秒。差了140倍。这还只是单列操作,要是你有几十个特征要生成,累积下来的时间差距就是小时级的。

Pandas向量化的几个常用模式

我个人习惯把常见的特征生成场景归纳成几种模式,遇到新需求直接套用:

  • 算术运算:直接对Series做加减乘除,比如 df['ratio'] = df['col_a'] / df['col_b']
  • 条件筛选:用 np.where 或 df.loc 替代if-else循环
  • 分组聚合:用 groupby().transform() 替代分组后逐行赋值
  • 窗口计算:用 rolling()、expanding() 替代滑动窗口循环
  • 字符串处理:用 .str 访问器替代逐行字符串操作

小技巧:如果你不确定某个操作能不能向量化,先查Pandas官方文档。90%的常见操作都有向量化版本。实在找不到,可以用 apply() 过渡,但记住apply本质还是循环,只是语法糖。

条件逻辑的向量化:np.where vs for-if

我曾经接手过一个项目,里面有个特征叫“用户等级”,逻辑是:消费金额大于1000为VIP,500-1000为银卡,否则为普通。原代码写了个三层if-else循环,处理50万行数据用了8秒。

改成 np.select 后:

import numpy as np

# 条件列表
conditions = [
    df['amount'] > 1000,
    (df['amount'] > 500) & (df['amount'] <= 1000)
]
# 对应结果
choices = ['VIP', '银卡']

# 向量化条件赋值
df['level'] = np.select(conditions, choices, default='普通')

# 耗时:0.02秒

8秒到0.02秒,400倍的提升。你想想看,如果每天要跑一次特征流水线,这能省下多少时间?

分组特征的向量化:groupby + transform

生成分组统计特征时,很多人会写这样的循环:

# ❌ 错误示范
user_mean = {}
for uid in df['user_id'].unique():
    user_mean[uid] = df[df['user_id'] == uid]['amount'].mean()

for i in range(len(df)):
    df.loc[i, 'user_mean_amount'] = user_mean[df.loc[i, 'user_id']]

这个写法有两个问题:一是慢,二是代码丑。正确的做法是:

# ✅ 向量化分组
df['user_mean_amount'] = df.groupby('user_id')['amount'].transform('mean')

# 一行搞定,速度提升50倍以上

避坑指南:我曾经在分组后忘记加 transform,直接用了 groupby().mean(),结果返回的是去重后的结果,跟原DataFrame长度对不上。记住:要保留原行数,必须用 transform。

向量化操作的性能边界

当然,向量化也不是万能的。我遇到过两种情况,向量化反而不好使:

  • 内存不足:当数据量超过内存时,向量化操作会把整个数组加载到内存,导致OOM。这时候可以考虑分块处理或使用Dask。
  • 复杂业务逻辑:有些特征生成逻辑涉及多步条件判断、查表映射,用向量化写出来反而可读性差。这时候我建议用 apply 加 numba 加速,或者直接用Cython。

但话说回来,80%的特征生成场景,向量化都是最优解。先把这80%搞定,剩下的再考虑特殊优化。

本章知识体系

下面这张图总结了向量化操作的核心思路和常见模式:

向量化操作知识体系 向量化操作 C语言底层执行 连续内存 + CPU缓存 SIMD并行计算 五大常用向量化模式 算术运算 + - * / 条件筛选 np.where 分组聚合 groupby+transform 窗口计算 rolling/expanding 字符串处理 .str访问器 性能提升:50x ~ 400x(相比Python for循环) ⚠ 注意:内存不足时考虑分块处理;复杂逻辑可用numba加速

总结一下

向量化操作是特征生成性能优化的第一课,也是最立竿见影的一课。我建议你养成一个习惯:写任何数据处理代码之前,先问自己一句“这个能不能用向量化做?”

刚开始可能会不习惯,毕竟for循环的思维太根深蒂固了。但坚持一两周,你会发现向量化写法不仅快,代码也更简洁、更易读。这波投资,值。