
简介这份资源是2023年泰迪杯B题「产品订单需求预测」的一等奖完整参赛论文PDF格式面向参加数据挖掘竞赛、机器学习课程设计或从事需求预测方向的学生与从业者。论文全文不含附录共55页系统梳理了从特征工程到组合预测的完整建模链路适合作为赛题复盘与算法进阶的参考范本。压缩包内仅含1个PDF文件大小约2.58MB轻量便于下载与打印研读。目前已有1747人学习下载热度较高。内容上论文先通过Pearson相关性、趋势拟合与方差分析定量刻画价格、区域、销售方式、品类、节假日及促销节等因素对订单需求的影响再依次构建CART、SVR、KNN、随机森林、LightGBM与LSTM等模型并利用GA智能寻优确定超参最终提出LGB-GA-LSTM组合预测方案按周粒度预测的MAPE低至0.497%。读者可从中获取完整的赛题解题框架、特征处理技巧、模型对比思路与组合优化策略对提升预测类竞赛实战能力有直接帮助。1. 泰迪杯B题产品订单需求预测一等奖方案里真正值钱的是什么2023年泰迪杯B题的产品订单需求预测放到今天看依然是一道很典型的工业级时序回归题。它给的是多产品、多仓库、多时间粒度的历史订单数据要求预测未来若干周期的订单需求量。听起来像普通的销量预测但做过的人都知道这类赛题的难点从来不在模型本身而在于数据里藏着的缺失、促销脉冲、节假日错位和产品冷启动。一等奖方案里真正值钱的不是某个花哨的网络结构而是一整套从特征工程到模型融合的工程化思路。GA-LSTM负责捕捉时序里的长程依赖LGB处理高维稀疏特征和类别变量集成学习把两者的偏差互补掉这套组合在当年被大量队伍验证过。如果你现在手里有类似的订单预测任务或者正准备复现泰迪杯B题的方案下面这套从数据到融合的落地路径可以直接照着走。2. 数据理解与特征工程订单预测的地基怎么打2.1 先搞清楚订单数据的四层结构泰迪杯B题的数据通常包含四个维度产品、仓库、时间、订单量。很多新手一上来就急着套LSTM结果被数据里的坑拖死。我一般会先做三件事画每个产品的订单时序图、统计每个仓库的订单分布、检查时间轴上有没有断点。订单数据最常见的结构性问题有三个一是某些产品在某些仓库完全没有历史记录属于冷启动二是促销期间订单量会出现脉冲式尖峰如果不做标记模型会把它当成正常波动学进去三是节假日导致的周内模式偏移比如春节前后两周的订单节奏和平时完全不同。处理这些问题第一步是构建一张完整的产品-仓库-时间笛卡尔积表把缺失的订单量填成0还是NaN取决于业务含义。如果某个产品在某个仓库确实没有上架填0会引入错误信号这时候应该用掩码标记。我一般会加一列is_active来表示该产品在该仓库是否处于活跃销售状态。import pandas as pd import numpy as np # 构建完整时间索引 date_range pd.date_range(start2021-01-01, end2023-06-30, freqD) products df[product_id].unique() warehouses df[warehouse_id].unique() # 笛卡尔积 full_index pd.MultiIndex.from_product( [products, warehouses, date_range], names[product_id, warehouse_id, date] ) full_df pd.DataFrame(indexfull_index).reset_index() # 左连接原始订单数据 full_df full_df.merge(df, on[product_id, warehouse_id, date], howleft) # 标记活跃状态如果该产品在该仓库历史上有过订单则视为活跃 active_pairs df.groupby([product_id, warehouse_id])[order_qty].sum().reset_index() active_pairs[is_active] 1 full_df full_df.merge(active_pairs[[product_id, warehouse_id, is_active]], on[product_id, warehouse_id], howleft) full_df[is_active] full_df[is_active].fillna(0) # 活跃但缺失的订单量填0不活跃的保持NaN full_df.loc[full_df[is_active] 1, order_qty] \ full_df.loc[full_df[is_active] 1, order_qty].fillna(0)这段代码的核心逻辑是区分“真实零订单”和“未上架”。is_active为1时缺失值填0是合理的因为产品在售但当天没人下单is_active为0时保留NaN后续建模时直接排除这些样本。参数上freqD按天聚合如果原始数据是周粒度就改成W。注意date_range的起止时间要和训练集、测试集对齐别把未来信息漏进来。2.2 时序特征滑窗、滞后和滚动统计怎么设订单预测的特征工程里滑窗统计是最有效的一类。我一般会构造三组特征滞后特征、滚动均值和滚动标准差。滞后特征直接取前1、7、14、28天的订单量滚动窗口取7天和14天。这里有个细节滚动统计必须用shift(1)把当前天排除掉否则就是标签泄露。# 按产品和仓库分组后构造时序特征 full_df full_df.sort_values([product_id, warehouse_id, date]) grouped full_df.groupby([product_id, warehouse_id]) # 滞后特征 for lag in [1, 7, 14, 28]: full_df[flag_{lag}] grouped[order_qty].shift(lag) # 滚动统计排除当前天 for window in [7, 14]: full_df[frolling_mean_{window}] grouped[order_qty].transform( lambda x: x.shift(1).rolling(window, min_periods1).mean() ) full_df[frolling_std_{window}] grouped[order_qty].transform( lambda x: x.shift(1).rolling(window, min_periods1).std() ) # 周内模式星期几 full_df[day_of_week] full_df[date].dt.dayofweek full_df[is_weekend] (full_df[day_of_week] 5).astype(int) # 月度特征 full_df[month] full_df[date].dt.month full_df[day_of_month] full_df[date].dt.daymin_periods1保证序列开头不会因为窗口不满而全是NaN。滞后阶数选1、7、14、28是因为订单数据通常有周周期和月周期7和14覆盖周模式28覆盖月模式。滚动标准差用来捕捉订单波动性促销期这个值会明显变大模型能借此识别异常时段。day_of_week和is_weekend是低成本高收益的特征很多队伍忽略了这个结果周末订单模式完全学不到。2.3 促销和节假日的标记策略促销标记不能只靠日期硬编码因为不同产品的促销时间可能不同。我一般会从订单量本身反推如果某天订单量超过该产品过去30天均值的3倍就标记为促销日。节假日则用现成的日历库但要注意调休。春节、国庆这种长假期前后各扩展3天作为影响窗口。# 基于历史均值的促销标记 full_df[promo_flag] 0 for (pid, wid), group in full_df.groupby([product_id, warehouse_id]): hist_mean group[order_qty].shift(1).rolling(30, min_periods7).mean() threshold hist_mean * 3 mask group[order_qty] threshold full_df.loc[group.index[mask], promo_flag] 1 # 节假日标记以春节为例实际使用时替换为对应年份的日期 holiday_dates pd.to_datetime([2022-01-31, 2022-02-01, 2022-02-02, 2023-01-21, 2023-01-22, 2023-01-23]) full_df[is_holiday] full_df[date].isin(holiday_dates).astype(int) # 节前节后窗口 for offset in [-3, -2, -1, 1, 2, 3]: col_name fis_holiday_offset_{offset} shifted_dates holiday_dates pd.Timedelta(daysoffset) full_df[col_name] full_df[date].isin(shifted_dates).astype(int)促销标记用滚动均值做阈值避免了手动标注的偏差。min_periods7是为了防止序列开头均值不稳定导致误标。节假日窗口特征让模型能学到节前备货和节后回落的模式这在订单预测里非常关键。注意shift(1)在促销标记里也要用否则当天的订单量会参与自己的阈值计算。3. GA-LSTM模型遗传算法怎么优化LSTM的超参数3.1 为什么用GA而不是网格搜索LSTM的超参数空间很大隐藏层单元数、学习率、dropout率、batch size、时间步长。网格搜索在5维空间里随便一组合就是几百次训练每次训练几分钟到几十分钟根本跑不完。遗传算法通过选择、交叉、变异来搜索超参数空间通常30到50代就能收敛到不错的解。我一般把种群大小设成20迭代50代每代保留最优的2个个体直接进入下一代。GA的适应度函数就是验证集上的RMSE。注意要用时间序列交叉验证不能随机划分否则未来信息泄露会让适应度虚高。具体做法是用前80%的时间做训练后20%做验证或者用滚动窗口做多折验证。3.2 GA-LSTM的完整实现下面是一个可运行的GA优化LSTM的代码框架。输入数据是前面构造好的特征矩阵输出是最优超参数组合和对应的验证集RMSE。import numpy as np import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from sklearn.preprocessing import MinMaxScaler from deap import base, creator, tools, algorithms import random # 准备时序数据将特征矩阵转成LSTM需要的3D格式 def create_sequences(data, target, time_steps): X, y [], [] for i in range(len(data) - time_steps): X.append(data[i:itime_steps]) y.append(target[itime_steps]) return np.array(X), np.array(y) # 假设feature_matrix是N x F的数组target是N维订单量 time_steps 14 scaler MinMaxScaler() feature_scaled scaler.fit_transform(feature_matrix) target_scaled scaler.fit_transform(target.reshape(-1, 1)).flatten() X, y create_sequences(feature_scaled, target_scaled, time_steps) # 时间序列划分 split int(len(X) * 0.8) X_train, X_val X[:split], X[split:] y_train, y_val y[:split], y[split:] # 定义适应度函数 def evaluate(individual): units, lr, dropout_rate, batch_size individual units int(units) batch_size int(batch_size) model Sequential([ LSTM(units, input_shape(time_steps, X_train.shape[2]), return_sequencesFalse), Dropout(dropout_rate), Dense(1) ]) model.compile(optimizertf.keras.optimizers.Adam(learning_ratelr), lossmse) history model.fit(X_train, y_train, epochs20, batch_sizebatch_size, validation_data(X_val, y_val), verbose0) val_loss min(history.history[val_loss]) return (val_loss,) # 遗传算法配置 creator.create(FitnessMin, base.Fitness, weights(-1.0,)) creator.create(Individual, list, fitnesscreator.FitnessMin) toolbox base.Toolbox() toolbox.register(attr_units, random.randint, 32, 256) toolbox.register(attr_lr, random.uniform, 0.0001, 0.01) toolbox.register(attr_dropout, random.uniform, 0.1, 0.5) toolbox.register(attr_batch, random.randint, 16, 128) toolbox.register(individual, tools.initCycle, creator.Individual, (toolbox.attr_units, toolbox.attr_lr, toolbox.attr_dropout, toolbox.attr_batch), n1) toolbox.register(population, tools.initRepeat, list, toolbox.individual) toolbox.register(evaluate, evaluate) toolbox.register(mate, tools.cxBlend, alpha0.5) toolbox.register(mutate, tools.mutGaussian, mu0, sigma0.1, indpb0.2) toolbox.register(select, tools.selTournament, tournsize3) # 运行GA population toolbox.population(n20) result, log algorithms.eaSimple(population, toolbox, cxpb0.6, mutpb0.3, ngen30, verboseTrue) best_individual tools.selBest(population, k1)[0] print(f最优超参数: units{int(best_individual[0])}, lr{best_individual[1]:.5f}, fdropout{best_individual[2]:.3f}, batch{int(best_individual[3])})这段代码里evaluate函数是GA的核心它接收一个个体四个超参数训练LSTM并返回验证集最小loss。attr_units范围32到256覆盖了从小到大的容量attr_lr从0.0001到0.01这是Adam常用的学习率区间attr_dropout从0.1到0.5防止过拟合attr_batch从16到128。cxBlend是混合交叉适合连续变量mutGaussian做高斯变异indpb0.2表示每个基因有20%概率变异。ngen30是迭代代数实际跑的时候可以加到50。注意epochs20是为了加速GA搜索最终用最优参数训练时应该加大到100并加EarlyStopping。3.3 LSTM的输入输出维度怎么对齐LSTM要求输入是3D张量(样本数, 时间步长, 特征数)。时间步长我一般取14或28对应两周或四周的历史窗口。特征数就是前面构造的所有特征列。输出是1维的订单量预测值。这里有个容易翻车的地方如果特征里包含了滞后特征时间步长就不能取太大否则滞后特征和窗口内的历史信息会重复。我一般会把滞后特征和原始特征分开处理LSTM只吃原始时序滞后特征交给LGB。训练时用EarlyStopping监控验证集losspatience10如果10轮不下降就停止并恢复最优权重。学习率用ReduceLROnPlateaufactor0.5patience5。这两个回调能省不少调参时间。4. LGB与集成学习把偏差和方差一起压下去4.1 LGB为什么适合订单预测的表格特征LightGBM在表格数据上的表现通常比深度学习更稳尤其是当特征里有大量类别变量和缺失值时。订单预测的特征矩阵里产品ID、仓库ID、星期几、月份这些都是类别特征LGB可以直接用categorical_feature参数处理不需要one-hot编码。另外LGB的训练速度比XGBoost快很多在GA搜索超参数时优势明显。我一般用LGB处理三类特征滞后特征、滚动统计特征、日历特征。LSTM处理原始时序和促销标记。两者的预测结果做加权融合权重用验证集上的RMSE反比来确定。import lightgbm as lgb from sklearn.metrics import mean_squared_error # 准备LGB的输入展平后的特征矩阵 feature_cols [col for col in full_df.columns if col not in [product_id, warehouse_id, date, order_qty]] X_lgb full_df[feature_cols].values y_lgb full_df[order_qty].values # 时间序列划分 split_idx int(len(X_lgb) * 0.8) X_train_lgb, X_val_lgb X_lgb[:split_idx], X_lgb[split_idx:] y_train_lgb, y_val_lgb y_lgb[:split_idx], y_lgb[split_idx:] # LGB参数 params { objective: regression, metric: rmse, boosting_type: gbdt, num_leaves: 63, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, min_data_in_leaf: 20, verbose: -1 } # 类别特征列索引 cat_features [feature_cols.index(col) for col in [day_of_week, month, product_id_encoded, warehouse_id_encoded] if col in feature_cols] train_data lgb.Dataset(X_train_lgb, labely_train_lgb, categorical_featurecat_features) val_data lgb.Dataset(X_val_lgb, labely_val_lgb, categorical_featurecat_features, referencetrain_data) model_lgb lgb.train(params, train_data, num_boost_round1000, valid_sets[val_data], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)]) # 验证集预测 y_pred_lgb model_lgb.predict(X_val_lgb, num_iterationmodel_lgb.best_iteration) rmse_lgb np.sqrt(mean_squared_error(y_val_lgb, y_pred_lgb)) print(fLGB验证集RMSE: {rmse_lgb:.4f})num_leaves63控制树的复杂度订单预测里这个值不宜太大否则容易过拟合。learning_rate0.05配合num_boost_round1000和早停是比较稳的组合。feature_fraction0.8和bagging_fraction0.8引入随机性提升泛化。min_data_in_leaf20防止叶子节点样本太少。类别特征直接传列索引LGB内部会做目标编码比one-hot更高效。4.2 加权融合GA-LSTM和LGB怎么配比融合权重不是拍脑袋定的。我一般用验证集上的预测结果做线性回归让模型自己学权重。具体做法是把两个模型的验证集预测值作为特征真实值作为标签训练一个只有两个系数的线性模型系数就是融合权重。from scipy.optimize import minimize # 假设y_pred_lstm是GA-LSTM在验证集上的预测 def fusion_loss(weights): w1, w2 weights blended w1 * y_pred_lstm w2 * y_pred_lgb return np.sqrt(mean_squared_error(y_val_lgb, blended)) # 约束权重和为1 cons ({type: eq, fun: lambda w: w[0] w[1] - 1}) bounds [(0, 1), (0, 1)] result minimize(fusion_loss, [0.5, 0.5], boundsbounds, constraintscons) w_lstm, w_lgb result.x print(f融合权重: LSTM{w_lstm:.3f}, LGB{w_lgb:.3f}) # 最终预测 y_pred_final w_lstm * y_pred_lstm w_lgb * y_pred_lgb rmse_final np.sqrt(mean_squared_error(y_val_lgb, y_pred_final)) print(f融合后RMSE: {rmse_final:.4f})用scipy.optimize.minimize做约束优化保证权重非负且和为1。实际跑下来LGB的权重通常在0.6到0.7之间LSTM在0.3到0.4之间。如果LSTM权重超过0.5说明时序特征非常强这时候可以检查一下LGB的特征里是不是漏了关键滞后项。融合后的RMSE一般比单模型低5%到10%这个提升在竞赛里就是几十名的差距。4.3 集成学习里的Stacking和Blending怎么选除了加权融合Stacking是另一种常见做法。用LGB和LSTM的预测值作为新特征再训练一个元学习器通常用线性回归或小LGB。Stacking的好处是能学到非线性融合关系坏处是容易过拟合尤其是验证集不够大的时候。我的经验是如果验证集样本超过5000条用Stacking否则用加权融合更稳。Blending是Stacking的简化版只用一个holdout集来训练元学习器不涉及交叉验证。在泰迪杯这种时间序列任务里Blending比Stacking更安全因为时间序列的交叉验证本身就容易泄露。我一般会留最后20%的时间做holdout前80%里再分一部分做验证。5. 避坑与排查订单预测里最容易翻车的五个地方5.1 标签泄露滚动特征没shift导致验证集RMSE虚低现象验证集RMSE低到0.01提交后测试集RMSE直接翻倍。原因滚动均值和滚动标准差没有用shift(1)排除当前天模型在训练时看到了当天的订单量相当于开卷考试。解决所有滚动统计必须加shift(1)滞后特征本身已经是过去值不需要额外shift。检查方法是把验证集最后一天的滚动均值打印出来和真实值对比如果完全一样就是泄露了。5.2 冷启动产品预测全为NaN现象某些产品在验证集里预测值全是NaN导致整体RMSE无法计算。原因这些产品在训练集里没有历史记录LSTM的输入序列全是NaNLGB的滞后特征也是NaN。解决对冷启动产品用同类产品的平均订单曲线做初始化或者直接用该产品所属类目的均值填充。LGB对NaN有天然处理能力但LSTM不行必须在输入前填充。我一般用该类目下所有产品的中位数填充冷启动产品的滞后特征。5.3 GA搜索陷入局部最优现象GA跑了30代最优个体和初始种群差不多RMSE几乎没降。原因种群多样性不足或者变异率太低。mutpb0.3在4维空间里可能不够。解决把种群大小加到30到50变异率提到0.4交叉率保持0.6。另外可以在前10代用较大的变异步长后20代缩小步长做精细搜索。如果还是不行换用随机搜索做初始化把随机搜索的前10个最优解放进初始种群。5.4 LGB的类别特征编码后维度爆炸现象产品ID有几千个one-hot之后特征矩阵稀疏到无法训练。原因直接对高基数类别变量做one-hot。解决用LGB原生的categorical_feature参数或者做目标编码。目标编码要注意用交叉验证的方式计算否则会泄露标签。我一般用5折目标编码每折用其他4折的均值来编码当前折。5.5 融合权重在测试集上失效现象验证集上融合权重0.6/0.4效果很好测试集上反而不如单模型。原因验证集和测试集的时间分布不一致比如验证集在淡季测试集在旺季。解决用滚动窗口做多折验证每折都算一次融合权重取平均值。另外可以给融合权重加正则化限制在0.4到0.7之间避免极端权重。如果测试集有明显的季节偏移考虑在融合前对两个模型的预测做季节性调整。6. 从复现到提分一个被低估的后处理技巧很多队伍在模型融合之后就收工了其实后处理还能再榨出几个千分点的RMSE。订单预测里最有效的后处理是分位数映射。具体做法是把验证集上的预测值排序真实值排序然后建立一个从预测分位数到真实分位数的映射函数用这个函数调整测试集的预测分布。这个技巧在订单量有明显的季节波动时特别管用因为模型倾向于预测均值而真实分布往往有长尾。from scipy.interpolate import interp1d # 用验证集建立分位数映射 def quantile_mapping(y_pred_val, y_true_val, y_pred_test): # 排序 sorted_pred np.sort(y_pred_val) sorted_true np.sort(y_true_val) # 建立映射预测分位数 - 真实分位数 pred_quantiles np.linspace(0, 1, len(sorted_pred)) true_quantiles np.linspace(0, 1, len(sorted_true)) # 插值函数 mapping interp1d(pred_quantiles, sorted_true, kindlinear, bounds_errorFalse, fill_valueextrapolate) # 对测试集预测值做映射 test_quantiles np.searchsorted(sorted_pred, y_pred_test) / len(sorted_pred) y_pred_adjusted mapping(test_quantiles) return y_pred_adjusted # 应用分位数映射 y_pred_test_adjusted quantile_mapping(y_pred_lstm, y_val_lgb, y_pred_test)这段代码的逻辑是先把验证集的预测值和真实值分别排序建立预测分位数到真实分位数的插值函数。然后对测试集的每个预测值找到它在验证集预测分布中的分位数再用映射函数转换成调整后的值。interp1d的kindlinear保证映射平滑fill_valueextrapolate处理超出范围的值。注意这个技巧的前提是验证集和测试集的分布不能差太远如果差太远反而会引入偏差。我一般会先画一下验证集和测试集的预测分布直方图如果形状相似再用。另一个实用的后处理是整数化。订单量通常是整数如果模型输出的是连续值四舍五入到整数能小幅降低RMSE。但要注意如果订单量本身是聚合后的值比如周订单量整数化可能不适用。我一般会对比整数化前后的RMSE如果提升不到0.1%就不做避免引入不必要的偏差。最后说一个我自己的习惯每次跑完模型我都会把验证集上预测误差最大的20个样本单独拎出来看。这些样本往往对应着促销、缺货或者数据录入错误。如果能找到规律就加对应的特征如果找不到就接受它是噪声。这个习惯帮我避免了很多次“模型明明验证集很好但测试集翻车”的情况。希望帮到你。本文还有配套的精品资源点击获取