数据预处理优化:特征工程加速,数据清洗与转换效率提升技巧
做数据分类接口,说到底拼的不是模型多牛逼,而是你喂进去的数据干不干净、特征好不好。我见过太多团队花80%时间在调参上,结果预处理随便写个循环挨个跑——数据量一上来,接口直接卡死。今天咱们就聊聊,怎么把数据预处理这条腿练粗。
特征工程加速:别让代码成为瓶颈
特征工程是提升模型效果的关键,但如果写得糙,它就是拖垮接口的元凶。我个人习惯把特征工程分成三步:数值化 → 组合 → 筛选。每步都有加速套路。
向量化操作取代逐行循环
你想想看,一个特征编码如果写成这样:
# 别这么干
for i in range(len(df)):
df.loc[i, 'code'] = encode(df.loc[i, 'category'])
跑100万行,直接凉凉。换成向量化:
# 这样才对
df['code'] = df['category'].map(encoding_dict)
性能差异至少两个数量级。我在项目中遇到过几百兆的CSV,用循环跑了40分钟,换成向量化后不到1秒。嗯,那次真是印象深刻。
并行化特征交叉
特征交叉(比如年龄×性别)在高维场景下很常见。但别傻傻地嵌套循环:
# 低效写法
new_cols = []
for a in list_a:
for b in list_b:
new_cols.append(f'{a}_{b}')
改成 itertools.product 或者直接用 pandas 的广播机制,再配合多进程:
from multiprocessing import Pool
def build_cross(pair):
a, b = pair
return df[a] * df[b] # 示例:数值交叉
with Pool(4) as pool:
cross_features = pool.map(build_cross, pairs)
这里要注意:并行不是银弹,IO密集型任务用多进程反而慢。特征交叉属于计算密集型,并行效果拔群。
数据清洗效率提升:干掉那些脏数据
数据清洗占了预处理的一大半时间。我以前总觉得清洗就是写if-else,直到那次线上故障——接口返回了NaN,客户端直接崩溃……从那以后我学乖了,清洗也得讲效率。
缺失值批量填充
别一个一个列处理。用 fillna 的时候,根据不同列类型做统一策略:
| 列类型 | 推荐策略 | 代码示例 |
|---|---|---|
| 数值型 | 中位数填充 | df.fillna(df.median()) |
| 类别型 | 众数填充 | df.fillna(df.mode().iloc[0]) |
| 时间型 | 向前填充 | df.fillna(method='ffill') |
| 文本型 | 空字符串标记 | df.fillna('') |
一次性填充整张表:
fill_dict = {
'age': df['age'].median(),
'gender': 'unknown',
'join_date': method='ffill'
}
df.fillna(fill_dict, inplace=True)
比逐列填充快3~5倍,而且代码干净。
异常值检测的加速技巧
通常用IQR或者Z-score检测异常值。但不要对每列写同样的逻辑——封装成函数,然后用 apply 一次搞定:
def detect_outliers(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
return (series < Q1 - 1.5*IQR) | (series > Q3 + 1.5*IQR)
outlier_mask = df.select_dtypes(include=[np.number]).apply(detect_outliers)
df[~outlier_mask.any(axis=1)] # 过滤包含异常的行
转换效率提升:少复制,多复用
数据转换(比如归一化、类型转换、时间解析)是另一个吃性能的点。核心原则:避免隐式复制,善用inplace。
类型转换用 astype 而不是临时变量
# 坏习惯:复制两次
temp = df['price'].astype(float)
df['price'] = temp
# 好习惯:原地改
df['price'] = df['price'].astype(float)
你以为一样?其实第一个写法会产生中间数组,内存占用翻倍。在大数据场景下,这种隐形浪费很容易触发内存溢出。我曾经帮一个团队检查接口,发现他们每次请求都创建两个临时DataFrame——数据量一上500M,接口就报OOM。改成链式操作后,问题消失。
利用 categorical 类型降低内存
类别型特征如果用的是object类型,每个字符串都要单独存储。转换为category类型后,底层用整型索引,内存能降低70%以上:
df['city'] = df['city'].astype('category')
而且后续groupby、value_counts的速度也会翻倍。我个人习惯在数据加载后就把所有低基数的string列转成category,尤其是渠道来源、省份、等级这种字段。
延迟计算:能省则省
在特征工程中,有些计算并不需要立刻执行。比如你要对多个列做同样的缩放,可以用 pipe 把操作链延迟到最后一刻:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
def scale_pipeline(df, cols):
df[cols] = scaler.fit_transform(df[cols])
return df
# 不立刻执行,只记录操作
df_pipeline = df.pipe(lambda x: x).pipe(scale_pipeline, ['age', 'income'])
# 真正执行时才计算
df_result = df_pipeline
当然,pandas的链式调用并不是真正的延迟,但通过合理组织代码顺序、避免中间变量,能减少不必要的计算和内存占用。
避坑指南:我曾经踩过的坑
- 别一次读入全部数据 — 我曾经处理5亿条日志,直接pd.read_csv卡死了。后来用chunksize分批处理,配合多进程,效率提升10倍。
- 小心inplace的副作用 — 有些pandas操作(如
fillna)如果设置inplace=True,会返回None。如果你不小心把结果赋给变量,数据就丢了。我建议:宁可用链式操作,也别依赖inplace。 - 特征穿越 — 在时间序列场景下,用未来的数据生成特征(比如用明天的数值填充今天的缺失值),接口上线后直接翻车。切记:先按时间切分,再清洗。
1. 向量化 > 循环
2. 批量操作 > 逐列处理
3. 减少复制 > 追求简洁
4. 合理数据类型 > 默认类型
数据预处理优化的本质,是把每行代码的执行成本摊到数据量上。你每偷一次懒,接口就慢一次。下次预处理时,不妨多问自己一句:这个操作有没有更高效的写法?