资讯动态

机器学习特征工程实战:特征构建、数据拆分与防泄漏指南

发布时间:2026/9/9 17:09:43 来源:尧图企业网站定制
这次我们聊一个机器学习流程里最容易被低估、但直接决定模型上限的环节特征构建与特征拆分。先给一个结论同样一份数据同一个模型特征工程做得好与差最终精度差距可以非常大。很多同学模型调参调了很多轮效果始终上不去问题往往不在算法而在特征侧没做透。这篇文章围绕以下内容展开特征构建从原始数据里构造数值特征、类别特征、时间特征、交叉特征特征拆分训练集、验证集、测试集怎么切不同类型数据怎么切才不失真数据泄漏拆分顺序错了模型验证结果会虚高上线之后直接打回原形实际代码全部基于 pandas scikit-learn 给出可运行示例。适合下面这几类读者刚开始接触机器学习想知道特征工程从哪里下手做完基线模型后想通过特征构建继续提分处理时间序列数据纠结到底能不能直接train_test_split面试前补特征工程相关知识点。先看全文结构后面每一节都能直接落到代码上。1. 核心能力速览能力项说明所属阶段机器学习建模前的数据处理与特征工程阶段核心功能特征构建、特征变换、特征拆分、数据集划分、防数据泄漏常用工具pandas、numpy、scikit-learn关键方法数值特征聚合、类别特征编码、时间特征拆解、交叉特征、分层采样、时间序列切分运行环境本地 Jupyter Notebook / 脚本即可无 GPU 需求数据规模从几千行的教学数据到百万级工业数据都适用是否支持批量支持特征构建通过向量化操作批量作用于全表是否支持 API不涉及模型服务接口属于离线数据处理阶段典型输出可直接进入模型训练的特征矩阵 训练/验证/测试三个数据子集适合场景表格数据建模、风控、推荐、销量预测、异常检测等这一节想说明的意思很简单特征工程不是某个特定框架的功能而是一套数据处理方法论用最基础的工具就能实现。门槛不高但每一步都影响最终效果。2. 适用场景与使用边界特征构建和特征拆分几乎出现在所有有监督学习任务里。只要数据是表格形态有特征列、有目标列就需要考虑特征怎么生成、数据集怎么切。2.1 适用场景第一类是回归任务比如销量预测、房价预测。原始数据通常只有日期、门店、商品、价格、销量这些字段直接喂给模型模型只能学到非常粗粒度的规律。通过特征构建把“星期几”“是否为促销日”“近7天销量均值”“同类商品价格差”这些信息显式构造出来模型才有足够的信息去拟合。第二类是分类任务比如风控中的逾期预测、营销中的用户响应预测。这类场景特征列多且杂包含大量类别特征和缺失值。特征构建阶段需要做编码、分箱、组合特征拆分阶段要保证正负样本比例在训练集和测试集中一致。第三类是时间序列预测。这类任务最特殊因为数据自带时间顺序不能随机打乱再切分否则就会用到未来信息训练集和验证集之间形成数据泄漏。2.2 不适合什么场景特征工程不是万能的。如果原始数据本身信息量极少或者数据质量极差再怎么做特征构建也是无米之炊。另外深度学习模型在图像、文本、语音等领域可以直接从原始数据学习特征表示不需要手工做特征构建。本文方法更适合表格数据尤其是结构化业务数据。2.3 使用边界与合规提醒特征构建过程中凡是涉及用户ID、手机号、身份证等个人敏感信息必须做脱敏处理使用第三方数据做特征拼接时要确认数据授权范围拆分数据集时验证集和测试集只能用于评估不能参与特征构建的统计计算否则会引入泄漏在金融、医疗等强监管场景发布模型前要做特征稳定性评估和结果复核。3. 环境准备与前置条件特征工程依赖的库非常轻量不需要 GPU一台普通笔记本就能跑。3.1 基础依赖需要安装以下 Python 库pip install pandas numpy scikit-learn如果是在 Jupyter Notebook 中运行建议再加上 matplotlib 和 seaborn用于后面观察特征分布和验证曲线。3.2 环境检查导入库时如果报错先看是不是环境冲突或者 Python 版本问题。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, TimeSeriesSplit, KFold, StratifiedKFold print(pandas version:, pd.__version__) print(numpy version:, np.__version__) print(scikit-learn version:, __import__(sklearn).__version__)推荐使用 Python 3.9 以上版本。如果正在用 conda可以单独建一个环境避免依赖冲突。3.3 数据集准备后续代码示例使用一个模拟的电商销售数据集。这里直接生成一份包含日期、门店、商品类别、价格、销量等字段的数据方便复现import pandas as pd import numpy as np np.random.seed(42) n_rows 10000 df pd.DataFrame({ order_date: pd.date_range(2023-01-01, periodsn_rows, freqh), store_id: np.random.choice([S001, S002, S003, S004], sizen_rows), category: np.random.choice([electronics, clothing, food, books], sizen_rows), price: np.round(np.random.uniform(10, 500, sizen_rows), 2), cost: np.round(np.random.uniform(5, 300, sizen_rows), 2), quantity: np.random.randint(1, 10, sizen_rows), customer_tier: np.random.choice([normal, silver, gold], sizen_rows) }) df[sales_amount] df[price] * df[quantity] df.head()这份数据有 10000 行包含时间列、类别列和数值列适合演示特征构建和特征拆分。4. 特征构建的常用方法与代码实现特征构建的目标是让模型看到更丰富的输入信号。原始数据里的字段往往是最底层的记录值模型很难直接理解业务含义我们需要把这些信息转化为统计量、分类编码、时间周期等。4.1 数值特征构建数值特征是最容易扩展的一类。常见做法包括简单数学变换取对数、取平方、取比值统计聚合按某列分组计算均值、最大值、最小值、标准差累计特征累计求和、累计均值差异特征当前值减去历史值。示例代码# 简单特征派生 df[price_diff] df[price] - df[cost] df[profit_margin] (df[price] - df[cost]) / df[price] df[log_price] np.log1p(df[price]) # 分组聚合特征 df[category_price_mean] df.groupby(category)[price].transform(mean) df[store_quantity_std] df.groupby(store_id)[quantity].transform(std) df[store_sales_rank] df.groupby(store_id)[sales_amount].rank(methoddense)注意transform方法不会改变索引长度可以安全赋值回原 DataFrame。这里用log1p而不是log是为了避免价格为 0 时出现无穷大。4.2 类别特征构建类别特征不能直接喂给大多数机器学习模型需要转换为数值形式。sklearn 提供OrdinalEncoder和OneHotEncoder手工方式也能快速实现。# 手工标签编码 df[category_code] df[category].astype(category).cat.codes # One-Hot 编码 from sklearn.preprocessing import OneHotEncoder ohe OneHotEncoder(sparse_outputFalse) category_encoded ohe.fit_transform(df[[category]]) category_df pd.DataFrame( category_encoded, columns[fcategory_{cat} for cat in ohe.categories_[0]] ) df_encoded pd.concat([df, category_df], axis1)如果类别列基数特别大不建议直接 One-Hot会导致特征维度爆炸。此时可以考虑目标编码也就是用类别对应的目标均值代替原始类别值但目标编码在拆分不当的情况下更容易引入泄漏使用时要非常谨慎。4.3 时间特征构建时间列是特征工程里信息密度很高的一块但经常被忽略。常见时间特征包括年、月、日、星期几、小时是否为周末、是否为月初/月末距上一个促销日/节假日的天数连续时间点之间的间隔。示例代码df[order_date] pd.to_datetime(df[order_date]) df[year] df[order_date].dt.year df[month] df[order_date].dt.month df[day] df[order_date].dt.day df[dayofweek] df[order_date].dt.dayofweek df[hour] df[order_date].dt.hour df[is_weekend] df[dayofweek].isin([5, 6]).astype(int) # 时间差特征 df[row_time] df[order_date] df[time_diff] df[order_date].diff().dt.total_seconds().fillna(0)时间特征在销量预测、流量预测类任务中提升非常明显尤其是星期几和是否节假日这两个字段。4.4 交叉特征构建交叉特征是把两个或两个以上原始特征组合起来捕捉单一特征无法表达的信息。典型例子是“商品类别 × 门店”组合用来表达某个门店在某品类上的销售能力。df[store_category] df[store_id] _ df[category] # 如果类别组合的基数仍然可控可以直接用数值组合 df[price_quantity_ratio] df[price] / df[quantity].replace(0, np.nan)交叉特征不是越多越好每加一组交叉特征都会增加维度。建议先基于业务经验判断哪些字段之间有交互关系再进行组合不要无脑做全排列。4.5 特征构建的完整流程小结上面几段代码组合起来就得到了一列新的特征集。到这里要注意特征构建完成后应该先检查一下特征相关性和缺失情况不是特征越多越好。print(新增特征后的列数:, df.shape[1]) print(缺失值统计:) print(df.isnull().sum()[df.isnull().sum() 0])如果发现大量缺失值需要决定是填充、删除还是做成缺失标记特征。缺失值处理本身也是特征工程的一部分不要留到最后交给模型硬扛。5. 特征拆分与数据集划分实践特征拆分在这里特指“把数据集按行分成训练、验证、测试多个子集”。这是建模前最关键的一步因为拆分策略直接决定验证结果是否可信。5.1 常规随机拆分如果数据没有明显的时间顺序且类别分布均匀可以直接使用train_test_split。from sklearn.model_selection import train_test_split X df.drop(columns[sales_amount, order_date]) y df[sales_amount] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) X_train, X_val, y_train, y_val train_test_split( X_train, y_train, test_size0.2, random_state42 ) print(X_train.shape, X_val.shape, X_test.shape)上面的代码实际上做了两层拆分先分出 20% 作为测试集再在剩余 80% 里分出 20% 作为验证集最终训练、验证、测试比例约为 64:16:20。5.2 分层拆分如果目标列是分类变量且类别不平衡直接随机拆分可能导致训练集和测试集中某一类样本比例偏差很大。这时使用StratifiedKFold或train_test_split的stratify参数。from sklearn.model_selection import train_test_split # 构造一个分类目标 df[high_sales] (df[sales_amount] df[sales_amount].median()).astype(int) X df.drop(columns[sales_amount, order_date, high_sales]) y df[high_sales] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(训练集正样本占比:, y_train.mean()) print(测试集正样本占比:, y_test.mean())分层拆分能保持训练集和测试集中正负样本比例一致避免因采样偏差导致验证结果失真。5.3 时间序列拆分当数据按时间顺序收集时不能使用随机拆分否则测试集会混入时间上更早的样本。正确的做法是“按时间切块”前一段时间做训练后一段时间做验证。df df.sort_values(order_date).reset_index(dropTrue) # 按时间顺序切分 cutoff_1 int(len(df) * 0.7) cutoff_2 int(len(df) * 0.85) train_set df.iloc[:cutoff_1] val_set df.iloc[cutoff_1:cutoff_2] test_set df.iloc[cutoff_2:] print(训练集时间范围:, train_set[order_date].min(), -, train_set[order_date].max()) print(测试集时间范围:, test_set[order_date].min(), -, test_set[order_date].max())时间序列拆分最大的意义在避免数据泄漏。比如用前 7 天训练预测后 1 天随机拆分就可能出现训练集中包含后一天的数据验证时模型已经“见过”未来数据。5.4 分组拆分还有一种常见场景同一用户或同一门店的多次记录可能同时出现在训练集和测试集导致模型记忆个体特征而非学习通用规律。比如订单数据里同一个store_id有多条记录随机拆分后同一个门店的订单可能被分到训练集和测试集两边模型相当于见过这个门店的历史规律测试指标会虚高。解决方式是用GroupKFold按门店或用户分组拆分from sklearn.model_selection import GroupKFold gkf GroupKFold(n_splits5) X df.drop(columns[sales_amount, order_date]) y df[sales_amount] groups df[store_id] for train_idx, val_idx in gkf.split(X, y, groupsgroups): train_groups df.loc[train_idx, store_id].unique() val_groups df.loc[val_idx, store_id].unique() overlap set(train_groups) set(val_groups) print(f训练门店数: {len(train_groups)}, 验证门店数: {len(val_groups)}, 重叠门店: {len(overlap)})输出显示重叠门店为 0说明同一门店的样本不会同时出现在训练集和验证集里。这种拆分方式在推荐系统和风控场景中尤其重要。6. 交叉验证与防数据泄漏交叉验证本质上是一种多次拆分的验证机制目的是更稳定地评估模型效果。6.1 K 折交叉验证KFold把数据切成 K 份每次用 K-1 份训练、1 份验证循环 K 次。优点是每个样本都被用作过验证数据缺点是不能直接用于时间序列数据。from sklearn.model_selection import KFold from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error kf KFold(n_splits5, shuffleTrue, random_state42) scores [] X df[[price, cost, quantity]] y df[sales_amount] for fold, (train_idx, val_idx) in enumerate(kf.split(X)): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_val) rmse mean_squared_error(y_val, y_pred, squaredFalse) scores.append(rmse) print(fFold {fold 1}, RMSE: {rmse:.4f}) print(f平均 RMSE: {np.mean(scores):.4f})如果数据量不大K 折交叉验证比单次拆分更可靠。6.2 时间序列交叉验证时间序列数据要用TimeSeriesSplit它的切分方式保证训练集永远在验证集之前。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): print(fFold {fold 1}: 训练样本 {len(train_idx)}, 验证样本 {len(val_idx)})TimeSeriesSplit不会打乱顺序每次训练集都比上一次更大模拟的是“不断用过去预测未来”的过程。6.3 数据泄漏最容易被忽略的问题数据泄漏是特征工程中最隐蔽的错误。泄漏的本质是训练过程中使用了“本不该看到”的信息。常见泄漏场景在全量数据上做标准化或填充再拆分数据集用包含目标列的聚合统计构造特征随机拆分时间序列数据用同一个用户的不同记录同时进入训练和验证集。下面这段代码演示了一个典型错误# 错误示范在拆分前就对全量数据做标准化 from sklearn.preprocessing import StandardScaler scaler StandardScaler() # 这时 scaler 看到了测试集的均值和方差信息已经泄漏 X_scaled scaler.fit_transform(X) X_train, X_test train_test_split(X_scaled, test_size0.2, random_state42)正确做法是先拆分再在训练集上fit然后在训练集和测试集上分别transformX_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这一条建议记住所有基于全量数据计算的统计量都会造成泄漏包括均值、标准差、最大值、最小值、分位数。6.4 目标编码与泄漏的边界目标编码的做法是用某一类别对应目标变量的均值来替换类别值。比如计算每个门店的平均销量然后作为门店特征。# 风险做法直接用全量数据计算目标均值 df[store_target_mean] df.groupby(store_id)[sales_amount].transform(mean)上面的代码在训练和预测时都用了目标变量信息属于典型的泄漏。如果确实要用目标编码推荐在每一折交叉验证内部重新计算不能直接全量计算后复用。from sklearn.model_selection import KFold kf KFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in kf.split(df): train_df df.iloc[train_idx] val_df df.iloc[val_idx] # 只在训练折内计算目标均值 store_mean train_df.groupby(store_id)[sales_amount].mean() val_df[store_target_mean] val_df[store_id].map(store_mean)这种方式能最大程度降低泄漏风险但工程实现上更复杂。没有把握的情况下建议先避开目标编码。7. 资源占用与性能观察特征工程一般不涉及 GPU 计算但数据量大时仍然需要关注内存和耗时。7.1 内存占用pandas 默认使用 float64/int64 存储数据100 万行、50 列的数据就要占用约 400MB 内存。特征构建过程中还会产生中间副本内存占用可能翻倍。检查当前 DataFrame 内存print(内存占用: {:.2f} MB.format(df.memory_usage(deepTrue).sum() / 1024 ** 2))降低内存的常见方法是降精度df[price] df[price].astype(float32) df[quantity] df[quantity].astype(int8)7.2 特征构建耗时对于百万行数据groupby聚合操作通常是耗时大户。可以先跑小数据量验证逻辑再在完整数据上执行。另外尽量避免在循环里逐行操作pandas 的向量化操作性能远高于iterrows。下面给一个简单的压力测试方法import time start time.time() df[category_price_mean] df.groupby(category)[price].transform(mean) print(groupby transform 耗时: {:.4f} 秒.format(time.time() - start))如果多组特征构造分别耗时明显可以用多进程按特征组并行处理但这在特征工程阶段不是必须的优化方向。7.3 特征数量对模型的影响特征数量增长会直接增加训练耗时。树模型对特征数量相对鲁棒但线性模型和距离类模型受无关特征影响更大。特征构建完成后建议用特征重要性或相关性分析做一轮筛选而不是把所有特征一股脑丢进模型。corr df[[price, cost, quantity, sales_amount]].corr() print(corr[sales_amount].sort_values(ascendingFalse))如果发现某个新特征与目标变量相关性很低可以先从模型输入中移除观察模型指标变化。8. 常见问题与排查方法问题现象可能原因排查方式解决方案训练集和测试集效果差异大数据泄漏或拆分方式错误检查拆分代码是否在特征构建之后、确认时间序列数据是否随机拆分改为分层拆分、时间序列拆分或分组拆分模型过拟合严重特征数量太多或包含噪音特征查看特征重要性检查交叉验证指标做特征筛选、减少交叉特征、增加正则化测试集正负样本比例异常分类问题未使用分层采样打印训练集和测试集目标列均值train_test_split加stratifyy同一用户出现在训练集和测试集未做分组拆分检查用户ID在训练集和测试集中的交集改用GroupKFold或按时间切分时间序列预测使用随机拆分导致指标虚高拆分时打乱了时间顺序打印训练集与测试集时间范围查看是否有重叠改用TimeSeriesSplit或手动按时间切块特征构建后出现大量 NaN分组中出现空组或除数为 0df.isnull().sum()检查缺失列填充缺失值或增加缺失标记特征数据量大、groupby 很慢pandas 内存占用过高或循环逐行处理查看耗时与内存占用避免iterrows改用向量化操作、降低数据精度、分批处理目标编码后模型效果虚高使用全量数据目标统计量检查特征构造代码是否在拆分前执行在每折交叉验证内部重新计算目标编码这些是特征工程里出现频率最高的一批问题。每次模型效果异常先不要急着换算法先检查特征拆分和泄漏问题。9. 最佳实践与使用建议特征构建和特征拆分的坑很多是跑完一轮完整流程才发现的。下面几条建议能在前期就帮你降低返工成本。9.1 第一版先做最简特征基线不要一开始就堆大量特征。先拿两个原始特征加上一个最简单的时间特征跑一个最简模型记录指标。后续每加一组特征对比一次指标变化。只有加入后指标提升明显才保留这组特征。这样做的原因很实际特征数量一多排查问题时会非常混乱不确定是哪个特征引入的问题。保持最小可运行配置定位问题快很多。9.2 拆分逻辑优先于特征构建写文档在实际项目中经常有人写完了整套特征代码后才意识到拆分方式不对又回头改特征代码。建议写代码前先明确三个问题数据是否有时间属性是否必须顺序切分是否存在同一实体多条样本是否需要分组拆分目标变量是否不平衡是否需要分层采样。这三个问题没确定前不要动手做特征构建。9.3 特征、输入、输出分目录管理工程化项目建议按目录组织避免到处散落临时文件data/ raw/ # 原始数据 processed/ # 特征构建后的数据 splits/ # 训练/验证/测试子集 features/ build_features.py split_data.py分目录管理在批量任务中尤其重要跑完特征构建后直接落盘后续训练脚本统一读取出问题时也方便追溯哪一步数据生成异常。9.4 批量特征任务要加日志如果使用脚本批量处理特征任务建议给每个关键步骤加日志记录数据类型、行列数、缺失值数量和耗时。下面给一个简单的日志示例import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) logger.info(加载原始数据shape%s, df.shape) df build_features(df) logger.info(特征构建完成新增列数%dshape%s, df.shape[1] - 7, df.shape)在批量任务中日志是定位问题最直接的抓手至少要看得到每一步运行是否成功。9.5 发布前必须做效果复核无论是竞赛还是业务上线效果复核都不能省。建议在发布前做以下几项检查训练集、验证集、测试集的时间范围和类别分布是否符合预期特征构建代码是否在每次训练前统一执行是否存在拆分后再凑特征的情况是否对测试数据重复使用了训练数据计算的编码器或标准化参数涉及用户或实体级数据时是否确认了授权范围和数据脱敏。10. 总结与下一步特征构建和特征拆分是整个机器学习流程里门槛最低、但坑最多的一环。核心就三件事构建有用的新特征、用不泄漏的方式切分数据、在交叉验证里验证模型稳定性。最值得先动手验证的是 4.1 到 4.4 的特征构建代码把你自己的业务数据跑一遍观察新增特征与目标变量的相关性变化。然后重点检查你的拆分逻辑如果你的数据带时间属性不要直接train_test_split如果数据有用户ID、门店ID这类实体字段优先考虑GroupKFold。最容易踩的坑是数据泄漏尤其是目标编码和全量标准化。遇到模型验证效果很好、上线效果差距大的情况第一个怀疑对象就是泄漏。后续可以继续扩展的方向包括特征选择方法、自动化特征工程工具、以及如何把特征工程流程封装成可复用模块。先把这篇文章里的拆分思路和防泄漏逻辑吃透后面的建模工作会顺利很多。建议收藏备用下次做特征工程时直接对着代码过一遍。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价