做数据这一行,干得越久越认同一个观点:特征决定上限,模型只是逼近这个上限。我见过不少人对着模型参数调了一整天,收益还不如老老实实多做两个靠谱特征来得实在。最开始入坑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('category'),尤其是当唯一值数量远小于行数时,内存优化效果极其明显。
我用一个简单的自动优化函数来处理这件事:
python复制def optimize_dtypes(df):
for col in df.columns:
col_type = df[col].dtype
if col_type != 'object':
c_min = df[col].min()
c_max = df[col].max()
if str(col_type)[:3] == 'int':
if c_min >= 0:
if c_max < 255:
df[col] = df[col].astype('uint8')
elif c_max < 65535:
df[col] = df[col].astype('uint16')
else:
df[col] = df[col].astype('uint32')
elif str(col_type)[:5] == 'float':
df[col] = df[col].astype('float32')
else:
if df[col].nunique() / len(df) < 0.5:
df[col] = df[col].astype('category')
return df
注意df[col].nunique() / len(df) < 0.5这个阈值,不是所有字符串列都适合转category,如果每行都是唯一值,转成category反而更耗内存。判断的唯一依据是唯一值比例。
2.3 重复值处理:别让脏数据悄悄拉偏模型
重复值问题在高维表中表现得很隐蔽,很多人只会在显式的完全重复行时调用df.drop_duplicates(),但实际业务里更多遇到的是"部分列重复"的情况。比如同一用户在同一时间戳下有多条行为记录,这在日志型数据里极其常见。如果不去重,模型会错误地认为这个用户有更多行为、更高活跃度,从而产生偏差。
处理重复值的正确方式是先想清楚"重复"的定义——是整行重复,还是基于某些键的重复?基于业务含义去去重,而不是盲目去重。举个例子,在用户行为数据中,用户ID+事件类型+发生时间三个字段通常能唯一定位一条行为记录。如果同一个用户在一分钟内对同一商品点击了5次,是否需要合并成一条?取决于你建模的目标是"该用户是否购买",还是"点击率预估"。前者可以合并压缩,后者则可能需要保留频次信息。
python复制# 完全重复行
df.drop_duplicates(inplace=True)
# 基于关键列去重,保留最后一条记录
df.drop_duplicates(subset=['user_id', 'event_type', 'event_time'], keep='last', inplace=True)
这里我用keep='last'是有讲究的:如果按时间顺序排列,最后一条往往是整个行为序列的状态终点,保留它更能反映用户当前的状态。当然,具体保留哪条应根据业务语义来决定,没有绝对的规则。
2.4 构造组合特征和时间窗口特征
单列信息经过清洗和类型优化之后,就可以进入特征构造阶段。这里说的特征构造不是拍脑袋加列,而是从业务逻辑和数据模式出发,从原始字段中“合成”新信息。
常见的特征构造方向:
- 组合特征:把两个或多个业务上有关联的字段拼在一起。例如“用户等级 + 设备类型”这种组合,往往比单独一个特征携带更多信息,因为高等级用户和小米用户的组合模式,可能和高等级用户与iPhone用户的组合模式完全不同。Sklearn的
PolynomialFeatures能做数值间交叉,但类别间的组合我更喜欢在Pandas里手动拼列再编码。 - 时间特征:时间戳本身模型无法理解,但“小时”“星期几”“是否工作日”“距上一次购买间隔多少天”这些衍生字段,则蕴含了极强的周期性信息。电商场景中,“距上次访问间隔时间”几乎是我每做必加的特征,它对识别用户流失倾向有显著帮助。
- 统计特征:按用户分组计算的均值、标准差、最大值、最小值、最近7天行为次数等,这类特征在用户行为建模中非常常见,但要注意计算效率和数据泄漏问题。
时间字段的处理有个容易忽略的操作:字符串转datetime之后要显式提取成分。很多人会用pd.to_datetime(df['time']),但转完之后忘记.dt.hour这类访问器,一直拿字符串做拆分,既慢又容易出错。正确写法:
python复制df['event_time'] = pd.to_datetime(df['event_time'])
df['hour'] = df['event_time'].dt.hour
df['weekday'] = df['event_time'].dt.weekday
df['is_weekend'] = df['event_time'].dt.weekday.isin([5, 6]).astype(int)
一个小小的提醒:一旦从datetime类型中提取出这些衍生特征,原始的时间戳列在建模阶段通常就可以丢弃了。保留它只会增加特征维度,而且datetime类型本身无法被直接送入模型。
3. Sklearn侧的特征变换:编码与缩放的标准姿势
3.1 类别特征的编码方法怎么选
数据清洗完成、新特征构造好之后,就轮到Sklearn上场了。第一步是处理类别型特征。很多初级选手直接用pd.get_dummies()做独热编码,这个做法在小特征维度的场景下没什么问题,但一旦类别基数很大(比如城市名、商品ID),会得到极其稀疏的宽表,内存和训练速度都会崩掉。
Sklearn官方推荐的方式是ColumnTransformer配合OneHotEncoder,而不是手动get_dummies,理由有三个:第一,ColumnTransformer可以记住每列的编码方式,在预测阶段对新的数据直接transform,不会出现列错位;第二,可以设置handle_unknown='ignore',避免线上出现训练集里没见过的类别时直接报错;第三,它支持在一个管道里同时处理多个不同类型的列,代码更整洁。
python复制from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
categorical_cols = ['device_type', 'os_version', 'channel']
preprocessor = ColumnTransformer(
transformers=[
('cat', OneHotEncoder(handle_unknown='ignore'), categorical_cols),
('num', 'passthrough', numeric_cols)
]
)
对于高基数类别特征(比如商品名称、地理位置等,类别数超过几十甚至上百),独热编码并不合适,我一般改用目标编码(Target Encoding)。目标编码的思路是用当前类别下目标变量的均值(可以加平滑)来替代这个类别本身,相当于把类别聚合成一个数值。Sklearn的TargetEncoder在新版本中已经加入了sklearn.preprocessing,可以直接用。但要注意,目标编码非常容易导致数据泄漏,必须放在交叉验证内部做拟合,而不是在全局数据上先编码再划分训练集和验证集。这个坑我踩过,后面在特征选择章节里细讲。
3.2 数值型特征的缩放:树模型不慌,线性模型慌
数值型特征是否需要标准化或归一化,取决于你最终用什么模型。树模型(随机森林、XGBoost、LightGBM)对特征尺度不敏感,做不做缩放对结果影响不大;但线性模型、SVM、KNN以及使用梯度下降优化的神经网络,则强烈依赖特征缩放。如果特征A的取值范围是0到1,特征B是0到100000,那在距离计算或梯度更新时,特征B会主导整个结果,模型学不到特征A的信息。
Sklearn提供了两种最常用的缩放方式:StandardScaler和MinMaxScaler。StandardScaler将数据变为均值为0、方差为1的标准正态分布形式,适用于特征分布接近正态的情况;MinMaxScaler将数据缩放到[0,1]区间,适用于特征有明确上下界的情况。
实际项目里,我处理数值特征的标准写法是:
python复制from sklearn.preprocessing import StandardScaler
num_cols = ['active_days', 'total_clicks', 'avg_session_time']
scaler = StandardScaler()
X_train_num = scaler.fit_transform(X_train[num_cols])
X_test_num = scaler.transform(X_test[num_cols])
注意这里的关键点:fit_transform做的是拟合并转换,用于训练集;transform只是按训练集的均值和方差做转换,用于测试集。绝对不能对测试集单独fit_transform,否则测试集的分布会被泄漏到模型里,造成验证指标虚高。这个细节是面试高频考点,也是实操里最常见的坑。
还有一点建议:如果特征的分布严重右偏(很多业务指标比如收入、点击量都有这个现象),可以在缩放之前先做log变换。Sklearn里可以用FunctionTransformer(np.log1p)把这一步嵌入管道。
3.3 用Pipeline把零散环节串成一条线
特征工程的目的是把一堆处理步骤组织好,而不是每次训练都在命令行里手工复现一遍。Sklearn的Pipeline机制就是干这个的:把编码、缩放、模型训练统一封装成一个对象,fit时自动对训练数据做拟合,predict时自动对输入数据做同样的变换。
这个机制在模型上线时价值最大。我遇到过一种很尴尬的现场:线下训练时各种特征处理都在Jupyter里手动执行,到了线上要加载模型做预测,却发现忘了保存编码器的映射关系,或者编码顺序发生了变化,导致特征错位,预测结果完全失控。用Pipeline之后,joblib.dump整个pipeline即可,加载后直接transform预测,彻底告别这种低级事故。
python复制from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('scaler', StandardScaler()),
('classifier', RandomForestClassifier(n_estimators=200, random_state=42))
])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
如果你还想在Pipeline里加“特征选择”步骤,直接把SelectKBest或RFE作为中间步骤插进去即可。这样整条链路从原始表到预测结果,一气呵成。
4. 特征选择:特征不是越多越好,冗余反而拖后腿
4.1 为什么在大数据场景下更要主动减特征
如果你有几百个特征,树模型训练速度可能还能接受,但当你面对上千维甚至更高维的稀疏特征时,训练效率和模型效果都会受影响。更重要的是,冗余特征会引入多重共线性和噪声,把模型的泛化能力拉低。特征选择的核心目标不是单纯减少数量,而是找到对目标变量最有解释力、彼此之间冗余度最小的组合。
有一个广为流传的说法是“树模型自带特征选择,不需要额外做”,这个说法部分正确——树模型确实在分裂时会挑选最有区分度的特征,但它并不知道哪些特征是重复信息。比如你有两个相关性高达0.98的特征,树模型会在不同树中随机选用它们,导致每个特征的重要性被平摊,模型对单一特征的依赖被稀释,可解释性和稳定性都会变差。所以即使是用树模型,适度做特征筛选依然有好处,尤其是特征数特别多时。
4.2 四类常用特征选择方法及适用场景
我整理了自己常用的四类特征选择方法,それぞれ适用的场景不同:
- 方差过滤(VarianceThreshold):思想很简单,某列特征在所有样本上的取值几乎一样,说明它没有区分能力,可以直接去掉。适用于大规模特征矩阵的预处理阶段,速度快,但只看方差不看与目标的关系,容易误删低方差却很有用的特征。
- 相关系数过滤:计算每个特征与目标变量的皮尔逊相关系数,保留绝对值较高的特征。适用于线性关系明显的数据,但方法本身只捕捉线性相关性,对非线性关系无能为力。
- 基于模型的特征重要性(Feature Importance):训练一个随机森林或梯度提升树,根据特征重要性排序截取Top N。这种方法能捕捉非线性关系和交互效应,最适合在拿到基线模型之后做二次筛选。我个人的习惯是先用全量特征跑一个LightGBM基线,然后看feature_importance,剔除掉重要性为0的特征,再重训。
- 递归特征消除(RFE):反复训练模型,每次剔除权重最小的特征,直到达到目标特征数。效果通常不错,但计算成本较高,大数据量级下要慎用。可以配合
LogisticRegression或SVM的coef来做快速评估,也可以配随机森林。
Sklearn里对应的实现分别有VarianceThreshold、SelectKBest配合f_regression或mutual_info_classif,以及RFECV。我通常的组合是:先用VarianceThreshold删掉零方差和近似零方差特征,再用相关分析删除共线性强的特征,最后用模型重要性做一轮精筛。
4.3 数据泄漏:特征选择里最隐蔽的坑
特征选择过程中最要命的问题,是数据泄漏,也就是把验证集或测试集的信息提前用到了训练过程里。典型场景是:先在全量数据上做特征选择,再切分训练集和测试集。这种做法虽然能让选出来的特征在“当时的测试集”上表现很好,但在真实线上数据上会大打折扣,因为你已经用“未来的数据”定了筛选方向。
正确的流程是:先切分训练集和测试集,然后在交叉验证的每一折内部做特征选择。Sklearn的Pipeline设计就是为了解决这个问题——把特征选择器放在Pipeline里,交叉验证时每次fold都会独立做特征选择,不会产生信息泄漏。
我早期做目标编码时对这个理解不够深,直接在全体数据上算类别的目标均值,然后编码给模型训练,线下用AUC测试结果高得离谱,一到线上表现就崩了。后来排查了很久才发现,目标编码里包含了测试集标签的均值信息,相当于模型在训练时已经见过了测试集的答案。从那以后,所有涉及目标变量的特征加工(目标编码、特征选择、SMOTE采样),我都一律放到交叉验证内部执行。
5. 完整实战:从用户行为日志到可训练特征矩阵
5.1 问题定义与数据样例
前面讲了这么多方法论,现在把它串成一个完整的实战案例。场景设定为:基于用户的浏览行为日志,预测该用户未来7天内是否会购买平台会员。输入数据包含用户基础信息、行为日志、商品浏览记录三类表。实际项目里这种多表关联很常见,这里为了演示方便,我会把行为日志做了预聚合,得到一个单表特征工程任务。
模拟数据可以直接生成,一共包含8个字段:user_id、注册天数、设备类型、操作系统、累计点击次数、累计浏览时长、最近一次活跃距今天数、目标变量是否购买。为了让演示贴近真实,我会故意往里面掺入缺失值和重复记录。
5.2 清洗与特征构造的完整代码
先说数据读取和清洗这一段。这里模拟读取一份CSV文件,在真实项目中这一步通常是接数仓或者直接读数据湖文件。
python复制import pandas as pd
import numpy as np
# 读取原始数据
df = pd.read_csv('user_behavior_data.csv')
print(df.shape)
print(df.info())
# 1. 去除完全重复行
df = df.drop_duplicates()
# 2. 缺失值处理
df['设备类型'] = df['设备类型'].fillna('unknown')
df['操作系统'] = df['操作系统'].fillna('unknown')
df['累计浏览时长'] = df['累计浏览时长'].fillna(
df.groupby('设备类型')['累计浏览时长'].transform('median')
)
df['时长缺失标记'] = df['累计浏览时长'].isna().astype(int)
# 3. 类型优化
df['注册天数'] = df['注册天数'].astype('int32')
df['累计点击次数'] = df['累计点击次数'].astype('int32')
df['累计浏览时长'] = df['累计浏览时长'].astype('float32')
# 4. 构造幅度特征
df['点击时长比'] = df['累计点击次数'] / (df['累计浏览时长'] + 1)
df['日均点击量'] = df['累计点击次数'] / (df['注册天数'] + 1)
第4步构造的两个特征就是典型的业务衍生特征:点击时长比衡量单位时长内的点击密集程度,日均点击量衡量用户活跃度。为什么不直接只用累计点击次数?因为注册天数差异太大,一个注册一年的用户累计点击10万次和一个注册三天的用户累计点击100次,不能直接比较。构造比率特征做归一化,比单纯用绝对数值更能反映行为的“密度”。
5.3 编码、缩放与特征选择的完整代码
处理完特征构造之后的代码流程,我建议全部放进一个Pipeline里,避免测试集泄漏。
python复制from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# 分类特征和数值特征分别定义
categorical_features = ['设备类型', '操作系统']
numeric_features = ['注册天数', '累计点击次数', '累计浏览时长', '时长缺失标记', '点击时长比', '日均点击量']
X = df.drop('是否购买', axis=1)
y = df['是否购买']
# 先切分,再做任何有监督的变换
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
preprocessor = ColumnTransformer(
transformers=[
('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features),
('num', StandardScaler(), numeric_features)
]
)
pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('feature_selection', SelectKBest(f_classif, k=15)),
('classifier', RandomForestClassifier(n_estimators=200, random_state=42))
])
pipeline.fit(X_train, y_train)
这里有个细节值得注意:SelectKBest选了k=15,但独热编码之后特征数会膨胀,最终实际特征数远超原始列数。所以k的选择不是拍脑袋,而是要看编码后的总维度。更稳妥的做法是用RFECV做自动选择,或者在Pipeline之外先跑一版全特征模型,看feature_importances_再定k值。
如果你用的是新版本sklearn,还可以直接把TargetEncoder插入Pipeline,但一定要记得把它放在交叉验证内部使用——只要放在Pipeline里,Sklearn会自动保证每折训练时只对本折数据做拟合,这就已经规避了目标编码的泄漏问题了。
6. 必经的那些坑:我的排查记录与心得
6.1 特征工程顺序引发的地狱级Bug
特征工程里最容易出问题的不是某个API不会用,而是顺序搞错了。train_test_split应该在第一道工序之前做,而不是特征加工全部完成后才做。凡是有监督的变换(目标编码、特征选择、归一化时用到统计量的场景)都必须遵守这个顺序。
我踩过最惨的一次:把所有特征加工(包括对缺失值做条件填充时按全量数据计算的均值和目标编码)跑完后才划分训练测试集,结果线下AUC高达0.95,上线后直接掉到0.72。排查了整整两天,最后发现是填充时用了全量数据的中位数——这个中位数里面包含了测试集的信息。从那以后,我的代码第一行永远是切分数据,再进入特征工程环节。
6.2 大数据量下的性能卡点排查方案
Pandas在处理大数据时最怕的就是循环遍历每一行。初学者经常写出for i in range(len(df))这种代码,几百万行数据跑起来能让人等到怀疑人生。Pandas的核心优势在于向量化操作,也就是整列一起运算,而不是一行一行的Python循环。对行的遍历应该尽量替换成apply或者直接向量化表达式,能不用循环就不用循环。
还有一个容易被忽略的卡点是datetime解析。pd.to_datetime在几百万行数据上非常慢,如果原始日志中的时间戳格式统一,我通常会先尝试用pd.to_datetime(df['time'], format='%Y-%m-%d %H:%M:%S')指定格式,速度能比自动推断快好几倍。
在高并发特征提取的场景里,Python的多进程反而会引发性能问题,因为Pandas的底层使用了很多C扩展,多进程worker之间复制大数据集的开销非常大。实测下来,对单张DataFrame做复杂变换时,多进程往往不如直接靠向量化和优化dtype来得快。真正的提速要点是减少不必要的副本、缩小数据量、用category替代字符串,这几点前面已经说过了。
6.3 验证特征工程效果的小技巧
每次做完一批新特征,我建议先不要急着堆进模型里,而是单跑一个快速对比:使用同样一组超参数,分别在“加新特征之前”和“加新特征之后”的数据上训练一个轻量模型,看验证指标的变化。如果指标没有明显提升甚至下降,说明新特征要么是噪声,要么和已有特征高度共线,这时候就要考虑删除或做进一步变换。
另外,在提交最终模型之前,我会用feature_importances_做个快速的可解释性检查。如果某个业务上重要的特征在模型中的重要性极低,可能是编码方向不对、缺失值太多,或者被另一个信息高度重叠的特征遮蔽了。这类检查能帮你在最终版本发布前发现很多潜在问题。
这套Pandas+Sklearn的特征工程流程,说不上有多高深,但胜在体系完整、每一环都有章可循。数据清洗、类型优化、缺失值设计、特征构造、编码变换、缩放、特征选择,按这个顺序走一遍,产出的特征矩阵质量和代码的可维护性都靠谱。我从一开始踩坑无数,到现在形成固定套路,最大的感受是:特征工程没有炫技的空间,比的往往是耐心、细致和对业务的理解。希望这篇文章能帮你少走一些我走过的弯路。
