1. 初识Kaggle零售销售预测挑战
第一次接触Kaggle的Store Sales时间序列预测比赛时,我完全被数据量吓到了。300多万条销售记录,横跨54家门店、33种商品大类,还要整合油价、节假日等外部因素。这可比教科书上的"某城市月度用电量预测"复杂多了。
这个比赛的数据来自厄瓜多尔连锁超市Favorita,目标是预测未来15天每家门店每种商品大类的销售额。数据包里包含几个关键文件:
- train.csv:2013-2017年的历史销售数据
- stores.csv:门店地理位置和类型信息
- oil.csv:每日国际油价(厄瓜多尔是石油出口国)
- holidays_events.csv:当地节假日和特殊事件
提示:时间序列预测比赛中,理解业务背景和数据关系比算法选择更重要。比如油价波动对食品销售的影响程度,节假日促销的实际效果等。
2. 数据清洗的实战技巧
2.1 缺失值处理的正确姿势
原始数据中最棘手的是oil.csv里的油价缺失。直接均值填充会失真,因为油价本身波动剧烈。我的解决方案是:
# 前向填充+可视化验证
df_oil['dcoilwtico'] = df_oil['dcoilwtico'].fillna(method='ffill')
plt.plot(df_oil['date'], df_oil['dcoilwtico'])
plt.title('油价走势(填充后)')
更隐蔽的坑在transactions.csv的交易额数据——测试集里该字段全是NaN。后来发现训练集中交易额为0的日子也被记录为NaN,需要用0填充:
df_transactions.fillna(0, inplace=True)
2.2 特征工程的组合拳
合并多表数据时,我设计了一套特征组合策略:
- 时空特征 :将门店坐标转换为区域编码,捕捉地理位置影响
- 时间分解 :把日期拆解成年、月、周、星期等周期性特征
- 外部因素 :油价波动率、节假日类型(全国性/地区性)
- 业务特征 :促销商品占比、门店类型组合
# 时间特征生成示例
df_train['date'] = pd.to_datetime(df_train['date'])
df_train['day_of_week'] = df_train['date'].dt.dayofweek
df_train['is_weekend'] = df_train['day_of_week'].isin([5,6]).astype(int)
3. 模型选择的进化之路
3.1 从Baseline模型起步
先用最简单的随机森林建立baseline,发现几个关键现象:
- MAE在70-100之间波动
- 商品类型(family)的特征重要性最高
- 油价影响比预期小,可能需做滞后处理
from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor(n_estimators=100, max_depth=10)
rf.fit(X_train, y_train)
print(f"MAE: {mean_absolute_error(y_test, rf.predict(X_test))}")
3.2 时间序列专属特征
传统机器学习模型处理时间序列的三大法宝:
- 滞后特征(Lag Features) :过去7/14/28天的销售数据
- 滚动统计(Rolling Stats) :近30天均值/标准差
- 扩展窗口(Expanding Window) :历史累计平均值
# 创建滞后特征
for lag in [7, 14, 21, 28]:
df_train[f'sales_lag_{lag}'] = df_train.groupby(['store_nbr','family'])['sales'].shift(lag)
3.3 梯度提升树的威力
切换到XGBoost后效果显著提升,关键配置:
xgb_params = {
'n_estimators': 500,
'max_depth': 8,
'learning_rate': 0.05,
'subsample': 0.8,
'colsample_bytree': 0.8,
'objective': 'reg:squarederror'
}
model = xgb.XGBRegressor(**xgb_params)
model.fit(X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20)
4. 比赛提分的秘密武器
4.1 目标变量变换技巧
原始销售额严重右偏,进行对数变换后模型效果提升15%:
# 对数变换+1避免零值
df_train['sales'] = np.log1p(df_train['sales'])
4.2 魔法特征:星期均值
最有效的特征竟是"该商品在该门店历史上同星期几的平均销售额":
weekday_avg = df_train.groupby(['store_nbr','family','day_of_week'])['sales'].mean().reset_index()
weekday_avg.columns = ['store_nbr','family','day_of_week','weekday_avg_sales']
df_train = pd.merge(df_train, weekday_avg, on=['store_nbr','family','day_of_week'])
4.3 数据筛选的玄学
意外发现只用最近1年数据反而比全量数据效果更好,说明:
- 消费模式会随时间演变
- 太久远的数据可能产生噪声
- 需要平衡数据量和数据质量
recent_data = df_train[df_train['date'] > '2016-08-15']
5. 完整项目复盘与经验总结
整个项目走下来,最大的收获是认识到时间序列预测的复杂性。不同于结构化数据预测,它需要:
- 对业务周期性的深刻理解(比如厄瓜多尔每月15日和月底发薪日对销售的影响)
- 灵活的滞后特征设计(不同商品可能有不同的销售周期)
- 外部因素的合理量化(如何用数值表示节假日的影响程度)
最终我的方案在Kaggle排名前15%,核心秘诀是:
- 构建了超过50个时间相关特征
- 采用两层模型堆叠:第一层预测趋势,第二层修正残差
- 对不同类型的商品(食品/日用品)分别建模
注意:比赛后期尝试了LSTM等深度学习模型,但效果不如特征工程到位的XGBoost,说明在数据量不够大时,传统方法仍有优势。
时间序列预测就像拼乐高,需要把各种特征模块巧妙组合。下次如果再参加类似比赛,我会更早开始做交叉验证,避免在特征重要性评估上走弯路。

6834

被折叠的 条评论
为什么被折叠?



