做数据这一行,干得越久越认同一个观点:特征决定上限,模型只是逼近这个上限。我见过不少人对着模型参数调了一整天,收益还不如老老实实多做两个靠谱特征来得实在。最开始入坑Kaggle时我也犯过这个错误——总以为XGBoost调参就是一切,直到被排行榜上的高分方案反复教育,才发现人家的特征工程做得有多细。这里我不打算讲那种需要Hadoop集群才能跑得动的大规模分布式方案,而是聚焦大多数实际业务场景里最常用的组合:Pandas做数据清洗和特征加工,Sklearn做编码、缩放和特征选择。这套工具链覆盖面广,学习曲线平滑,从小型数据集到几千万行的单机任务都能撑住,而且从0到1搭完这套流程,后面换Spark或Dask也只是换一层壳而已。
这篇文章适合刚入门想系统掌握特征工程流程的人,也适合已经有项目经验但总觉得特征部分做得不够系统的同学。全文会围绕一个用户行为日志的实战案例展开,带着你从原始表一步步加工到模型输入矩阵,整个过程涉及缺失值处理、数据类型优化、时序特征构造、类别编码、特征缩放和特征选择,每一段都配有可直接复用的代码和踩坑记录。
1. 特征工程到底是什么,为什么要有一套固定流程
1.1 模型拼到最后拼的都是特征质量
在正式写代码之前,先把特征工程的定位捋清楚。所谓特征工程,就是把原始数据中那些散乱的、不规则的、包含大量噪音的信息,转化成机器学习模型能够理解且更有利于区分目标变量的数值形态。这个过程看似"只是数据预处理",实际上直接决定模型效果的上限——算法本身只是一套优化工具,它只能从你给它的特征里寻找规律,喂进去的是垃圾,出来的预测不可能是金子。
我见过很多人拿到一份数据后直接model.fit(X_train, y_train),然后发现效果奇差,就开始怀疑模型选错了、参数没调好。但实际上问题大概率出在特征侧:日期字段还是字符串、缺失值被简单填成了0、类别变量被塞进了树模型而没有做任何处理、分布严重偏斜的数值型字段没有做变换。这些都是特征工程没做到位的典型症状。
特征工程通常可以分为四个环节:数据清洗、特征构造、特征变换、特征选择。数据清洗解决"脏"的问题,特征构造解决"不够"的问题,特征变换解决"模型不认"的问题,特征选择解决"太多太杂"的问题。四者组合在一起,才构成一套完整的特征处理流程。
1.2 为什么选择Pandas+Sklearn这套组合
不少人在选工具时纠结于要不要上Spark、Flink这些大数据框架。我的建议很简单:先看数据量。如果你的数据在单机内存能承载的范围内(通常几千万行以内),Pandas的向量化操作效率非常高,而且生态成熟,写法灵活;Sklearn则提供了几乎全套的特征预处理和特征选择API,接口统一,和Pandas衔接顺滑。两者结合,可以在不引入额外基础设施的前提下解决绝大多数场景下的特征工程问题。
这也不是说大数据框架没用,而是说在数据量还没到那个量级之前,过度设计只会增加开发和维护成本。先拿Pandas+Sklearn把特征工程的方法论跑通,后面真遇到分布式需求时,你的知识也能直接迁移——因为特征工程的思路和数据结构设计,跟具体引擎关系不大。
在我自己的工作流里,数据读入后第一件事就是用df.info()和df.describe()快速掌握全貌,这两行命令能帮你迅速定位哪列是数值型、哪列是对象型、哪些字段缺失率极高。真正进入特征加工环节之前,先花五分钟看清数据长什么样,比什么都重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Pandas侧的特征加工:先把数据洗干净、造出新特征
2.1 缺失值处理不是简单填个0那么简单
缺失值处理是我见过翻车频率最高的环节。一个常见的错误是:不论什么字段、不论缺失原因,一律用df.fillna(0)解决。这样做的问题在于,模型会认为缺失值代表"数值为0",从而学到错误的规律。正确的做法是先给缺失情况分分类:数值型字段的缺失可能是数据未记录、设备离线、业务本身无值;类别型字段的缺失可能代表"无此项"或"未知状态",处理方式完全不同。
数值型字段我通常的做法是两路并行:第一路,保持缺失值本身作为一个信息,新增一列is_missing标记缺失状态,因为很多时候"缺失与否"本身就是一个有预测力的特征;第二路,对原特征做填充,填充策略视情况而定——数据分布近似正态且缺失率不高的字段用中位数填充,分布偏斜严重的字段用条件填充(比如按组内均值填充),有时也会做多重插补但实际项目中用得不多。
类别型字段的策略更简单直接:把缺失值单独编码成一个新的类别,比如"unknown"。这里给一个实操示例:
python复制import pandas as pd
df['device_type'] = df['device_type'].fillna('unknown')
df['user_rating'] = df['user_rating'].fillna(df.groupby('user_tier')['user_rating'].transform('median'))
df['rating_is_missing'] = df['user_rating_original'].isna().astype(int)
第二行代码用的是分组填充,而不是全局填充。原因是不同等级的用户评分分布差异明显,全局中位数会把高等级用户的特征往中间拉,损失区分度。这个细节在Kaggle中非常常见,很多高分方案的特征不是靠某个花哨算法取胜,而是靠这种贴着业务逻辑的精细化处理。
2.2 数据类型转换:把内存和效率一起优化
大数据场景下,Pandas的内存优化是一件不能忽视的事。默认情况下,pandas读取数值列时会分配int64或float64,对于一列取值只有0和1的二值特征,这浪费了8倍内存。我曾在一份3000万行的数据上做过测试,仅靠合理设置数据类型,内存占用从接近5GB降到1.5GB左右,运行效率提升了一倍不止。
具体优化手段有三板斧:
- 整数类型:如果列的最小值大于等于0且最大值小于255,可以转成
uint8;否则按实际范围选择uint16、int32等。 - 浮点类型:如果不需要那么高的精度,将
float64转成float32,内存直接减半,绝大多数机器学习场景下精度损失可以忽略。 - 类别类型:对字符串类别列,直接`astype
