查看全课大纲(第 35 / 35 节)

数据建模

约 6 分钟

小象实战讲义 · 特征工程精讲与实践

在完成了繁复而精密的特征工程之后,我们终于来到了数据建模环节。本节将基于我们精心构建的电商销量预测特征数据集,讲解如何使用树模型进行建模、调参、预测,并最终生成可提交至 Kaggle 的比赛结果。你将学习到从数据切分到模型训练、从超参数调优到结果提交的完整工业建模流程。

💡 核心导读

  • 数据切分策略:掌握基于时间序列的数据集划分方法,理解训练集、验证集和测试集在时序预测中的不同作用。
  • 模型训练与调参:了解使用 LightGBM 等 boosting 树模型进行建模的基本流程,学习网格搜索(Grid Search)和早停法(Early Stopping)等超参数调优技术。
  • 预测与结果提交:学习如何用训练好的模型对测试集进行预测,并按照比赛要求格式化输出提交文件。
  • 模型迭代与优化:理解如何通过分析特征重要性来指导后续的特征工程迭代,从而持续提升模型效果。

建模流程概述

建模是将特征工程成果转化为实际预测能力的关键一步。对于 Kaggle 的销量预测比赛,一个典型的建模流程包括以下几个步骤:

  1. 数据准备与切分:将包含所有特征和标签的最终数据集,按照时间顺序切分为训练集、验证集和测试集。
  2. 模型选择与初始化:选择适合的模型(如 LightGBM),并设置其初始参数。
  3. 超参数调优:使用验证集对模型的关键超参数进行调优,以寻找最佳配置。
  4. 模型训练与早停:使用训练集和验证集训练模型,并利用早停策略防止过拟合,确定最佳训练轮次。
  5. 预测与提交:使用调优后的模型对最终的比赛测试集进行预测,生成符合格式要求的提交文件。

整个流程是迭代的。我们可以根据模型在验证集和测试集上的表现,以及特征重要性分析,返回到特征工程阶段进行优化,形成闭环。

数据切分与准备

在时序预测问题中,我们不能随机打乱数据来划分数据集,因为这会破坏时间上的因果关系,导致“未来信息泄露”到训练中,使模型评估结果过于乐观。正确的做法是按时间顺序划分。

在我们的案例中,数据包含一个 date_block_num 字段,代表月份编号(例如,0 代表 2013年1月)。我们需要用历史数据训练,用更近的数据验证和测试。

import pandas as pd
import numpy as np

# 构造一个微型样例数据,模拟最终的特征数据集结构
# 假设 date_block_num 从 30 到 34,代表不同的月份
np.random.seed(42) # 固定种子仅用于生成索引,不用于印刷数据
date_blocks = np.repeat([30, 31, 32, 33, 34], 4)
shop_items = [(i, i*10) for i in range(1, 5)] * 5

demo_data = pd.DataFrame({
    'date_block_num': date_blocks,
    'shop_id': [s for s, _ in shop_items],
    'item_id': [i for _, i in shop_items],
    'feature1': [52.97, 44.52, 51.65, 41.77, 49.09, 55.12, 42.88, 47.11, 50.23, 46.89, 53.45, 48.76, 39.87, 54.32, 43.21, 56.78, 38.99, 57.12, 44.67, 52.34],
    'feature2': [22.85, 20.32, 20.64, 21.46, 15.86, 19.23, 24.10, 18.54, 22.01, 17.89, 25.43, 16.78, 23.65, 19.90, 21.12, 18.77, 24.88, 17.23, 22.45, 20.11],
    'item_cnt': [12.70, 8.52, 11.65, 7.77, 9.09, 13.12, 6.88, 10.45, 11.98, 9.34, 14.23, 8.90, 5.43, 12.67, 7.89, 15.12, 4.56, 14.89, 8.12, 11.45]
})

print("微型特征数据集样例(按时间排序):")
print(demo_data.sort_values('date_block_num').head(8))
微型特征数据集样例(按时间排序):
   date_block_num  shop_id  item_id  feature1  feature2  item_cnt
0              30        1       10     52.97     22.85     12.70
1              30        2       20     44.52     20.32      8.52
2              30        3       30     51.65     20.64     11.65
3              30        4       40     41.77     21.46      7.77
4              31        1       10     49.09     15.86      9.09
5              31        2       20     55.12     19.23     13.12
6              31        3       30     42.88     24.10      6.88
7              31        4       40     47.11     18.54     10.45

接下来,我们按照时间顺序划分数据集。通常,会用较早的时段作为训练集,临近的时段作为验证集,用于调参和模型选择,最后用最新的时段作为测试集,模拟未来预测。

# 按时间划分数据集 - 自包含版本
import pandas as pd
import numpy as np

# 重新构造与上一逻辑一致的微型数据,确保本块独立运行
np.random.seed(42)
date_blocks_local = np.repeat([30, 31, 32, 33, 34], 4)
shop_items_local = [(i, i*10) for i in range(1, 5)] * 5

demo_data_local = pd.DataFrame({
    'date_block_num': date_blocks_local,
    'shop_id': [s for s, _ in shop_items_local],
    'item_id': [i for _, i in shop_items_local],
    'feature1': [52.97, 44.52, 51.65, 41.77, 49.09, 55.12, 42.88, 47.11, 50.23, 46.89, 53.45, 48.76, 39.87, 54.32, 43.21, 56.78, 38.99, 57.12, 44.67, 52.34],
    'feature2': [22.85, 20.32, 20.64, 21.46, 15.86, 19.23, 24.10, 18.54, 22.01, 17.89, 25.43, 16.78, 23.65, 19.90, 21.12, 18.77, 24.88, 17.23, 22.45, 20.11],
    'item_cnt': [12.70, 8.52, 11.65, 7.77, 9.09, 13.12, 6.88, 10.45, 11.98, 9.34, 14.23, 8.90, 5.43, 12.67, 7.89, 15.12, 4.56, 14.89, 8.12, 11.45]
})

# 划分策略:date_block_num 30,31 为训练集,32为验证集,33为测试集,34为最终比赛测试集
train_df = demo_data_local[demo_data_local['date_block_num'].isin([30, 31])].copy()
val_df = demo_data_local[demo_data_local['date_block_num'] == 32].copy()
test_df = demo_data_local[demo_data_local['date_block_num'] == 33].copy()
final_test_df = demo_data_local[demo_data_local['date_block_num'] == 34].copy() # 最终提交的测试集

print(f"训练集大小: {train_df.shape}")
print(f"验证集大小: {val_df.shape}")
print(f"测试集大小: {test_df.shape}")
print(f"最终比赛测试集大小: {final_test_df.shape}")

# 准备特征和标签
feature_cols = ['feature1', 'feature2']
target_col = 'item_cnt'

X_train = train_df[feature_cols]
y_train = train_df[target_col]
X_val = val_df[feature_cols]
y_val = val_df[target_col]
X_test = test_df[feature_cols]
y_test = test_df[target_col]
X_final_test = final_test_df[feature_cols] # 最终测试集没有标签
训练集大小: (8, 6)
验证集大小: (4, 6)
测试集大小: (4, 6)
最终比赛测试集大小: (4, 6)

模型训练与超参数调优

我们选择使用 LightGBM 这类梯度提升树模型进行建模,因为它对表格数据通常有出色的表现,并且训练速度快。建模过程主要包括:初始化模型、调参、训练和预测。

超参数调优

调参是提升模型性能的关键步骤。常见方法有:

  • 网格搜索 (Grid Search):在指定的参数网格中穷举所有组合,寻找最优解。计算成本高。
  • 随机搜索 (Random Search):在参数空间中随机采样,更高效,可能更快找到较优解。
  • 早停法 (Early Stopping):对于 boosting 模型,在验证集性能不再提升时提前停止训练,防止过拟合,同时确定了最佳迭代轮次。

在实际项目中,我们可能先进行一轮粗粒度的网格搜索或随机搜索,找到有希望的区域,再结合早停法进行精细训练。

# 演示使用 sklearn 的 GridSearchCV 进行网格搜索的基本流程(以决策树为例)
# 注意:实际 LightGBM 调参需使用其原生接口或 sklearn API,此处为概念演示
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import GridSearchCV
import pandas as pd
import numpy as np

# 为演示自包含,重新构造与之前逻辑一致的微型数据
demo_data_for_search = pd.DataFrame({
    'feature1': [52.97, 44.52, 51.65, 41.77, 49.09, 55.12, 42.88, 47.11],
    'feature2': [22.85, 20.32, 20.64, 21.46, 15.86, 19.23, 24.10, 18.54],
    'item_cnt': [12.70, 8.52, 11.65, 7.77, 9.09, 13.12, 6.88, 10.45]
})
X_demo = demo_data_for_search[['feature1', 'feature2']]
y_demo = demo_data_for_search['item_cnt']

# 定义模型和参数网格
model = DecisionTreeRegressor(random_state=42)
param_grid = {
    'max_depth': [3, 5, None],
    'min_samples_split': [2, 5],
}

# 创建 GridSearchCV 对象
grid_search = GridSearchCV(estimator=model,
                           param_grid=param_grid,
                           cv=3, # 3折交叉验证
                           scoring='neg_mean_squared_error',
                           verbose=0)
grid_search.fit(X_demo, y_demo)

print("最佳参数组合:", grid_search.best_params_)
print("最佳交叉验证分数 (负MSE):", grid_search.best_score_)
最佳参数组合: {'max_depth': 3, 'min_samples_split': 5}
最佳交叉验证分数 (负MSE): -3.122534722222222

对于 LightGBM,重要的超参数可能包括:

  • num_leaves:叶子节点数量,控制模型复杂度。
  • subsample / colsample_bytree:对样本和特征的采样比例,有助于防止过拟合。
  • min_child_samples:叶子节点最小样本数。
  • learning_rate:学习率,通常配合 early_stopping_rounds 使用。

在项目实战代码中,我们使用网格搜索确定了超参数后,再使用早停法训练最终模型。

模型训练与预测

确定超参数后,我们就可以在训练集上训练模型,并在验证集上监控性能,使用早停策略。

# 模拟使用一个简单的模型(此处用线性回归代替 LightGBM 做演示)对最终测试集进行预测
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import pandas as pd
import numpy as np

# 重新构造划分好的数据集,确保本块独立
np.random.seed(42)
date_blocks_local2 = np.repeat([30, 31, 32, 33, 34], 4)
shop_items_local2 = [(i, i*10) for i in range(1, 5)] * 5

data_local2 = pd.DataFrame({
    'date_block_num': date_blocks_local2,
    'shop_id': [s for s, _ in shop_items_local2],
    'item_id': [i for _, i in shop_items_local2],
    'feature1': [52.97, 44.52, 51.65, 41.77, 49.09, 55.12, 42.88, 47.11, 50.23, 46.89, 53.45, 48.76, 39.87, 54.32, 43.21, 56.78, 38.99, 57.12, 44.67, 52.34],
    'feature2': [22.85, 20.32, 20.64, 21.46, 15.86, 19.23, 24.10, 18.54, 22.01, 17.89, 25.43, 16.78, 23.65, 19.90, 21.12, 18.77, 24.88, 17.23, 22.45, 20.11],
    'item_cnt': [12.70, 8.52, 11.65, 7.77, 9.09, 13.12, 6.88, 10.45, 11.98, 9.34, 14.23, 8.90, 5.43, 12.67, 7.89, 15.12, 4.56, 14.89, 8.12, 11.45]
})

train_df2 = data_local2[data_local2['date_block_num'].isin([30, 31])].copy()
val_df2 = data_local2[data_local2['date_block_num'] == 32].copy()
final_test_df2 = data_local2[data_local2['date_block_num'] == 34].copy()

X_train_combined = pd.concat([train_df2[['feature1', 'feature2']], val_df2[['feature1', 'feature2']]])
y_train_combined = pd.concat([train_df2['item_cnt'], val_df2['item_cnt']])

# 训练一个简单模型
simple_model = LinearRegression()
simple_model.fit(X_train_combined, y_train_combined)

# 对最终测试集进行预测
X_final_test2 = final_test_df2[['feature1', 'feature2']]
final_predictions = simple_model.predict(X_final_test2)

# 将预测结果与测试集ID组合,模拟提交文件格式
# 注意:真实比赛中,测试集有独立的ID列,预测值是未来月份的销量
submission_df = final_test_df2[['shop_id', 'item_id']].copy()
submission_df['item_cnt_month'] = final_predictions.clip(0, 20) # 按照比赛要求,将预测值裁剪到[0,20]区间

print("生成的提交文件样例(前4行):")
print(submission_df.head(4))
print(f"\n预测值范围: [{final_predictions.min():.2f}, {final_predictions.max():.2f}]")
print(f"裁剪后预测值范围: [{submission_df['item_cnt_month'].min():.2f}, {submission_df['item_cnt_month'].max():.2f}]")
生成的提交文件样例(前4行):
    shop_id  item_id  item_cnt_month
16        1       10        6.513562
17        2       20       13.878758
18        3       30        8.814121
19        4       40       12.115808

预测值范围: [6.51, 13.88]
裁剪后预测值范围: [6.51, 13.88]

在实际的实战代码中,我们使用调参后的 LightGBM 模型进行训练和预测,并将结果保存为 submission.csv 文件,即可提交至 Kaggle 平台获取评分和排名(注意:Kaggle 比赛通常一天最多可提交 5 次,超过后需等到第二天才能继续提交)。

迭代优化与特征重要性分析

建模完成并提交结果后,工作并未结束。如果比赛时间允许,我们可以进行迭代优化:

  1. 分析特征重要性:LightGBM、XGBoost、CatBoost 等模型都能输出特征重要性(如 gain、split)。这告诉我们哪些特征对模型预测贡献最大。
  2. 指导特征工程:
    • 强化重要特征:对于重要性高的特征,可以尝试构建其衍生特征、组合特征,挖掘更深层的信息。
    • 筛选不重要特征:对于重要性极低的特征,可以考虑移除,以简化模型、减少过拟合风险并提升训练速度。我们的最终特征数据集非常庞大,特征选择能有效节省计算资源。
  3. 借鉴优秀方案:可以去 Kaggle 该比赛的讨论区和优秀解决方案(Notebooks)中寻找新的思路和技巧,将其融入我们的流程中。

通过“特征工程 → 建模评估 → 分析反馈 → 特征优化”的闭环,我们可以一步步提升模型效果,在比赛排行榜上取得更好的名次。

📝 动手练一练

  1. 概念辨析:在时序预测问题中,为什么不能使用随机划分的方法来创建训练集和测试集?如果这样做了,可能会导致什么问题?
👉 点击查看参考答案

在时序预测中,数据点之间存在时间上的依赖关系,未来的值可能依赖于过去的值。随机划分会打乱时间顺序,导致模型在训练时可能“看到”了未来的信息(例如,用2014年的数据训练,但验证时却用2013年的数据),这称为数据泄露。这会使模型在验证集/测试集上表现出虚假的高精度,因为它在训练时已经间接“知道”了答案。当模型部署到真正的未来预测时,性能会大幅下降。因此,必须严格按照时间先后顺序划分数据。

  1. 动手实践:假设你从 LightGBM 模型中得到了一份特征重要性列表,排名前三的特征分别是 item_cnt_lag1(上月销量)、item_price(商品价格)和 shop_city_encoded(店铺城市编码)。请基于此,提出两种可能的特征工程优化方向。
👉 点击查看参考答案

基于特征重要性分析,可以尝试以下优化方向:

  • 方向一(强化重要特征):
    • 对 item_cnt_lag1(上月销量),可以尝试构建更丰富的滞后特征,如 item_cnt_lag2、item_cnt_lag3,或者计算滚动统计量,如过去3个月的平均销量 (item_cnt_lag1to3_avg)。
    • 对 item_price,可以构建价格变化率特征,如 price_change_ratio = (current_price - last_month_price) / last_month_price。
  • 方向二(特征组合):
    • 尝试将高重要性特征进行组合,例如创建 item_cnt_lag1 * item_price(上月销量与价格的交互项),或 item_cnt_lag1 / shop_avg_price(上月销量相对于店铺平均价格的比值),这些新特征可能捕捉到更复杂的模式。

本章小结

本节我们完成了电商销量预测案例的最后一步——数据建模。我们学习了如何将精心构建的特征数据集应用于实际的机器学习模型,掌握了从时序数据划分、模型训练调参到结果提交的完整流程。更重要的是,我们理解了建模不是终点,而是新一轮特征工程迭代的起点,通过分析模型表现和特征重要性,可以持续优化我们的解决方案。

📋 行动清单

  • 根据时间顺序,将你的特征数据集正确划分为训练集、验证集和测试集。
  • 使用 LightGBM 等模型进行训练,并尝试使用网格搜索和早停法进行超参数调优。
  • 用训练好的模型对比赛测试集进行预测,生成 submission.csv 文件,体验 Kaggle 提交流程。
  • 运行模型的特征重要性分析,思考并规划下一步的特征优化策略。

—— 小象教研组