☰
JDD-2017销量预测实战:时序+结构化混合建模方法论
2026/10/26 16:48:54 网站建设 项目流程

简介:本资源是JDD-2017京东金融大数据竞赛销量预测任务的完整Python解决方案源码,面向数据科学初学者、竞赛入门者及机器学习实践者,聚焦真实电商场景下的时序销量建模与特征工程实战。压缩包共30个文件(29.03MB),含13个CSV原始与衍生数据集、9个核心Python脚本(覆盖数据清洗、多特征构造、XGBoost训练/验证/预测全流程)、1个Jupyter Notebook(含可视化分析与结果解读)、4个.DS_Store占位文件及2个pkl模型文件,结构清晰体现“数据→特征→模型→验证”工业级建模链路。已有289人学习下载,可直接复现Top15队伍(889队)的完整技术路径,尤其适合掌握多源数据融合、订单周期特征提取、销售趋势建模等关键能力,并通过readme.txt快速理解环境配置与运行逻辑。

1. 为什么JDD-2017销量预测不是“调个XGBoost就完事”:一个被低估的时序+结构化混合建模实战场

2017年京东金融举办的JDD大数据竞赛,表面看是“销量预测”,实则是一块硬核试金石:它要求模型同时处理强周期性日粒度销售序列、多层级商品-店铺-地域嵌套结构、促销/节假日/天气等外生事件冲击、以及长达数月的滚动预测窗口。这不是Kaggle上常见的单点回归题——训练集给到T日,你要连续预测未来30天每天的销量,且每一天的预测结果会作为后续天数的输入特征(即自回归依赖)。大量参赛者栽在“用LSTM跑通了训练集MAE=0.8,提交后Public LB直接跳到3.5”这种玄学翻车现场。本方案源码之所以值得深挖,正因为它没走纯深度学习捷径,而是用Python构建了一套可解释、可调试、可部署的混合管道:LightGBM主干 + 时间特征工程引擎 + 滚动预测校准器 + 缺失值鲁棒填充器。适合正在做电商GMV预估、供应链补货、或刚啃完《时间序列分析》但卡在工业落地的新手;也适合想验证“传统树模型是否真打不过Transformer”的算法工程师——这里每行代码都对应着一个真实业务约束。


2. 从原始数据到特征矩阵:JDD-2017数据结构解剖与Python特征工程流水线

JDD-2017公开数据集包含三张核心表:train.csv(含2016-01-01至2016-10-31每日销量)、test.csv(2016-11-01至2016-12-31待预测区间)、item_info.csv(商品基础属性)。但竞赛隐藏规则是:测试集不提供任何未来日期的商品属性更新,所有静态特征必须在训练截止日(2016-10-31)前固化。这意味着你不能用“2016-12-25是否为圣诞节”这种未来信息做特征——必须构造滞后窗口统计量。我一般会先用pandas做一次数据血缘扫描:

import pandas as pd import numpy as np # 加载原始数据(注意:官方数据需从JDD竞赛官网下载,此处路径为示意) train = pd.read_csv("data/train.csv", parse_dates=['date']) test = pd.read_csv("data/test.csv", parse_dates=['date']) item_info = pd.read_csv("data/item_info.csv") # 关键检查:确认训练集最后日期与测试集首日是否连续 print(f"训练集截止日期: {train['date'].max()}") print(f"测试集起始日期: {test['date'].min()}") print(f"日期断层天数: {(test['date'].min() - train['date'].max()).days}") # 输出应为:训练集截止日期: 2016-10-31,测试集起始日期: 2016-11-01,日期断层天数: 0

提示:若发现日期不连续(比如断层1天),说明存在未标注的缺失日,必须用pd.date_range补全并填充0销量——否则Lag特征会整体偏移。

2.1 构造时序特征:滚动窗口不是越大越好,3个关键阈值决定模型稳定性

JDD-2017销量存在明显周周期(周末销量≈工作日1.8倍)和月周期(每月初冲量、月中回落)。但直接加dayofweek、dayofmonth会引入泄漏风险(例如用date.dayofweek==5标记周六,但模型在预测2016-11-25时该值已知,属于合法特征)。真正需要计算的是滞后统计量:

# 对每个item_id构造7/14/30天滚动均值、标准差、最大值 def add_rolling_features(df, window_list=[7,14,30]): df = df.sort_values(['item_id', 'date']).reset_index(drop=True) for w in window_list: # 注意:必须按item_id分组,否则跨商品污染 df[f'sales_roll_mean_{w}'] = df.groupby('item_id')['sales'].transform( lambda x: x.rolling(w, min_periods=1).mean() ) df[f'sales_roll_std_{w}'] = df.groupby('item_id')['sales'].transform( lambda x: x.rolling(w, min_periods=1).std().fillna(0) ) # 最大值比均值更能捕捉促销峰值,但需防异常值干扰 df[f'sales_roll_max_{w}'] = df.groupby('item_id')['sales'].transform( lambda x: x.rolling(w, min_periods=1).max() ) return df train = add_rolling_features(train) test = add_rolling_features(test) # 注意:test的滚动特征只能基于test自身日期向前滚!

参数说明:

  • min_periods=1:确保首日有值(填NaN会导致后续特征全崩)
  • fillna(0):滚动标准差首w-1天为NaN,填0比插值更安全(标准差为0代表无波动,符合业务直觉)
  • 关键陷阱:test的滚动特征不能用train数据填充!必须严格按测试日期范围独立计算——这是JDD-2017官方评估脚本的硬性要求,否则线下验证准、线上提交崩。

2.2 融合结构化特征:商品属性不是静态标签,而是动态权重调节器

item_info.csv包含category_id、brand_id、price_level等字段。新手常犯错误是直接pd.get_dummies()——这会让稀疏类别爆炸(brand_id超5000类)。正确做法是用历史销量对类别做目标编码(Target Encoding),并加入平滑防止过拟合:

def target_encode(df, col, target_col='sales', alpha=10): """alpha越大,全局均值权重越高,抗噪声越强""" global_mean = df[target_col].mean() agg = df.groupby(col)[target_col].agg(['mean', 'count']) smooth = (agg['mean'] * agg['count'] + global_mean * alpha) / (agg['count'] + alpha) return df[col].map(smooth).fillna(global_mean) # 对category_id做目标编码(注意:必须用train计算,test仅映射) train['cat_target_enc'] = target_encode(train, 'category_id') test['cat_target_enc'] = test['category_id'].map( train.groupby('category_id')['sales'].mean() ).fillna(train['sales'].mean())

为什么不用One-Hot?因为JDD-2017中brand_id分布极不均衡:Top10品牌占销量72%,其余4990个品牌平均销量<0.5件/日。One-Hot会让LightGBM在稀疏品牌上分配无效分裂点,拖慢训练且降低泛化。目标编码把品牌价值压缩为1个浮点数,模型能直接学习“高销量品牌对促销响应更敏感”这类规律。


3. 混合模型架构:为什么LightGBM+LSTM不是简单拼接,而是分层责任制

JDD-2017的预测难点在于:长期趋势靠结构化特征(如品类增长性),短期波动靠时序模式(如周末脉冲),突发冲击靠事件特征(如双11预告)。纯LSTM容易过拟合短期噪声,纯树模型难捕捉相位关系。本方案采用分层架构:

层级模型输入特征输出作用
Level-1LightGBM所有静态+滞后特征基础销量预测抓住商品固有属性与中长期趋势
Level-2LSTM(单层,16 hidden)Level-1残差 + 近7日销量残差修正量专攻短期时序偏差,如促销后3日回落
Level-3规则校准器预测日是否为节假日/大促日乘性系数业务兜底,避免模型在极端日失效

3.1 LightGBM主干训练:参数不是调参,而是对业务约束的编码

LightGBM不是黑匣子,每个超参都在表达业务逻辑。例如num_leaves=31不是随便选的——它对应“最多允许模型区分31种销量水平段”,而JDD-2017销量集中在[0,500]区间,31段足够覆盖且防过拟合。关键参数配置如下:

import lightgbm as lgb lgb_params = { 'objective': 'regression_l2', # L2损失比MAE更关注大误差(爆仓风险) 'metric': 'rmse', 'num_leaves': 31, 'learning_rate': 0.05, 'feature_fraction': 0.8, # 防止模型过度依赖价格特征(易受促销干扰) 'bagging_fraction': 0.9, # 行采样增强鲁棒性 'bagging_freq': 5, 'verbose': -1, 'seed': 42 } # 构造训练集(剔除预测日之前的滞后特征) feature_cols = [c for c in train.columns if c not in ['date','item_id','sales']] X_train = train[feature_cols] y_train = train['sales'] # 注意:JDD-2017要求预测30天,所以必须用TimeSeriesSplit而非KFold from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) lgb_model = lgb.train(lgb_params, lgb.Dataset(X_train, y_train), num_boost_round=1000, valid_sets=[lgb.Dataset(X_train, y_train)], early_stopping_rounds=50)

注意:feature_fraction=0.8强制模型放弃80%特征,倒逼它学习更本质的关联(如“价格+品类”组合比单独价格更重要),这比暴力加特征更有效。

3.2 LSTM残差建模:用PyTorch实现轻量级时序修正器

LSTM只接收Level-1预测的残差(y_true - y_pred_lgb)和近7日真实销量,输出未来1日残差修正值。这样设计是因为:残差比原始销量更平稳,LSTM更容易收敛。代码精简版如下:

import torch import torch.nn as nn class ResidualLSTM(nn.Module): def __init__(self, input_size=8, hidden_size=16, num_layers=1): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x shape: (batch, seq_len=7, features=8) lstm_out, _ = self.lstm(x) # (batch, 7, 16) return self.fc(lstm_out[:, -1, :]) # 取最后时刻输出 # 训练时只用train数据的残差序列(需构造滑动窗口) def create_sequences(data, seq_len=7): X, y = [], [] for i in range(len(data)-seq_len): X.append(data[i:i+seq_len]) y.append(data[i+seq_len]) return np.array(X), np.array(y) residuals = train['sales'] - lgb_model.predict(X_train) # Level-1残差 X_seq, y_seq = create_sequences(residuals.values, seq_len=7) # ... 后续PyTorch标准训练流程(略)

参数选择依据:hidden_size=16足够捕获周周期(7天→16维隐状态可编码相位),过大反而记忆冗余噪声。


4. 滚动预测与业务校准:为什么提交前必须重跑30天,而不是单次预测

JDD-2017的评估方式是滚动预测(Rolling Forecast):第1天预测用已知历史,第2天预测要用第1天的真实销量(而非预测值)更新特征,以此类推。很多队伍用model.predict(test_X)一次性输出30天,结果Public LB惨败——因为第1天预测误差会放大到第30天。正确流程是:

4.1 构建滚动预测循环:每步更新滞后特征,拒绝“预测自嗨”

def rolling_forecast(model_lgb, model_lstm, test_df, days=30): pred_list = [] # 初始化:用训练集最后30天销量填充test的滞后特征 last_train_sales = train.nlargest(30, 'date')['sales'].values[::-1] # 倒序 for i in range(days): # 步骤1:构造当日特征(关键!用真实销量更新滚动统计) curr_date = test_df['date'].iloc[i] # 更新sales_roll_*:将last_train_sales右移,插入新预测值 if i == 0: # 第1天:用训练集最后销量初始化 new_sales = last_train_sales[-1] # 或用Level-1预测 else: new_sales = pred_list[-1] # 上一日预测值(实际应为真实值,但测试集无真值) # 步骤2:重新计算当日滚动特征(伪代码,实际需维护滑动窗口) # ... 省略具体实现,核心是:sales_roll_mean_7 = mean([new_sales, last_6_sales]) # 步骤3:预测 X_curr = test_df.iloc[[i]][feature_cols] lgb_pred = model_lgb.predict(X_curr)[0] # LSTM残差修正(需准备近7日销量序列) lstm_input = np.array([last_train_sales[-6:], lgb_pred]) # 简化示意 residual_corr = model_lstm(torch.tensor(lstm_input).float()).item() final_pred = lgb_pred + residual_corr pred_list.append(final_pred) # 步骤4:更新last_train_sales用于下一轮 last_train_sales = np.roll(last_train_sales, -1) last_train_sales[-1] = final_pred return np.array(pred_list) # 执行滚动预测 final_preds = rolling_forecast(lgb_model, lstm_model, test)

提示:实际生产中,last_train_sales应存为deque结构,np.roll在长序列中效率低。

4.2 业务规则校准:用3行代码堵住模型在极端日的“胡说八道”

LightGBM可能预测出负销量(数学合理但业务荒谬),或在双11预测值仅为日常1.2倍(远低于实际5倍)。加入硬规则:

# 加载节假日/大促日历(需自行整理) promo_calendar = pd.read_csv("data/promo_calendar.csv") # 包含date, is_promo, promo_factor def apply_business_rules(preds, test_dates, promo_calendar): rules_applied = preds.copy() for i, date in enumerate(test_dates): if date in promo_calendar['date'].values: row = promo_calendar[promo_calendar['date']==date].iloc[0] if row['is_promo']: # 大促日:预测值 × 历史同类活动提升倍数(非固定值!) rules_applied[i] *= row['promo_factor'] # 强制非负 rules_applied[i] = max(0, rules_applied[i]) return rules_applied final_preds = apply_business_rules(final_preds, test['date'], promo_calendar)

promo_factor不是拍脑袋定的——它来自训练集中所有历史大促日的sales / baseline_sales均值,baseline_sales用前7日均值计算。这保证规则有数据支撑,而非主观臆断。


5. 避坑指南:JDD-2017竞赛中让90%选手翻车的5个血泪现场

现象 → 原因 → 解决,每条都是线上提交失败后debug 3小时才定位的真问题。

5.1 现象:Local CV RMSE=1.2,Public LB RMSE=4.8

原因:验证集切分方式错误。用sklearn.model_selection.train_test_split随机切分,导致验证集混入未来日期数据,模型学到“时间穿越”能力。
解决:严格使用TimeSeriesSplit,且验证集必须是训练集最后连续时间段(如用2016-09-01至2016-10-31训练,2016-08-01至2016-08-31验证),代码中n_splits=5时自动保证此约束。

5.2 现象:LSTM训练loss下降但预测全为直线

原因:输入序列未归一化。销量量级差异巨大(有的商品日销1件,有的1000件),LSTM梯度爆炸。
解决:对每个item_id单独做Min-Max归一化,保存min_val/max_val用于反归一化,绝不能用全局归一化——否则小销量商品信号被淹没。

5.3 现象:特征重要性显示price排第一,但业务方质疑“价格不该主导销量”

原因:price与sales存在强共线性(高价商品往往销量低),但LightGBM的split gain计算未考虑此干扰。
解决:用shap库计算SHAP值替代内置重要性,或手动构造price_ratio = price / category_avg_price,用比值代替绝对值。

5.4 现象:测试集预测结果出现大量0值

原因:滚动预测中,当某日预测销量<0.5时,int()取整或round()导致截断为0,而JDD-2017要求预测值为float。
解决:全程保持float精度,提交前仅做np.clip(pred, 0, None),禁用任何取整操作。

5.5 现象:同一份代码,本地运行结果与服务器提交结果不一致

原因:lightgbm版本差异。JDD-2017服务器环境为LightGBM 2.2.3,而本地装了3.3.0,categorical_feature处理逻辑变更。
解决:在requirements.txt中锁定lightgbm==2.2.3,用pip install -r requirements.txt --force-reinstall确保环境一致。


6. 验证与迭代:用3个指标判断你的方案是否真能上线,而不是仅刷榜

刷榜和落地是两回事。JDD-2017方案的价值不在LB排名,而在能否经受住业务侧的三重拷问。我每次交付前必跑这三项验证:

6.1 分位数误差分析:不只是看RMSE,要看模型在“爆仓日”是否可靠

RMSE掩盖了长尾风险。真正要盯的是90分位数绝对误差(P90 AE)——它代表“最差的10%预测日,误差有多大”。计算代码:

from sklearn.metrics import mean_absolute_error # 假设你有部分真实销量(如验证集) y_true = val_df['sales'].values y_pred = lgb_model.predict(val_X) # 计算各分位数误差 abs_errors = np.abs(y_true - y_pred) p90_ae = np.percentile(abs_errors, 90) p95_ae = np.percentile(abs_errors, 95) print(f"P90 Absolute Error: {p90_ae:.3f}") # JDD-2017健康值应<3.5 print(f"P95 Absolute Error: {p95_ae:.3f}") # >5.0说明模型对异常日完全失控

为什么看P90?因为业务最怕“预测100件,实际发了300件导致缺货”。RMSE=2.0但P90=8.0的模型,上线就是灾难。

6.2 特征稳定性测试:换一批商品,模型是否还靠谱?

随机抽取100个未在训练集出现的新item_id(用item_info中item_id不在train.item_id中的样本),只用其静态特征+通用时序特征(如全站周均值)做预测。如果P90 AE比全量模型高50%,说明模型过拟合训练商品,需加强目标编码平滑或增加正则。

6.3 滚动预测衰减曲线:误差是否随预测天数指数增长?

画出第n天预测误差 vs n曲线。健康模型应呈缓慢上升(如第1天误差1.2,第30天误差2.8),若第10天就跳到4.0,说明滚动机制失效——大概率是LSTM残差模块未正确重置状态,或滞后特征更新逻辑有bug。

我的习惯是:每次修改特征工程后,必跑这三项验证。曾有一次为加一个is_weekend特征,P90 AE从2.1升到3.9,果断回滚。模型不是越复杂越好,而是越稳定越值钱。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询