☰
订单需求预测实战:数据清洗、特征工程与时间序列验证全流程
2026/10/22 2:31:14 网站建设 项目流程

简介:2023年泰迪杯B题一等奖参赛论文《产品订单需求预测》完整PDF,面向数学建模竞赛参赛者、数据分析与供应链预测方向学习者。论文以企业订单需求预测为主线,先利用Pearson相关性分析、趋势拟合模型和方差分析,从价格、区域、销售方式、品类、时间、节假日、促销节与季节等维度定量评估各因素影响;随后基于特征工程构建CART回归、SVR、KNN、随机森林、LightGBM与LSTM等机器学习及深度学习模型,并采用GA智能寻优算法确定超参数,进一步提出LGB-GA-LSTM组合预测框架,完整覆盖数据预处理、异常值剔除、缺失值填充、特征构造、模型训练与对比评价的赛题求解路径。资源包为单个PDF文件,大小2.58MB,全文不含附录共55页,结构完整,兼具方法研究与工程实现。已有1749人学习下载,适合需要系统掌握订单预测建模方法、参考竞赛获奖论文写作思路,或在实际业务中构建需求预测方案的读者借鉴。

1. 为什么一份获奖论文最值得抄的是数据策略而不是模型

产品订单需求预测这类赛题,表面上比谁的模型精度高,实际上一等奖论文和优秀奖论文之间的模型差距往往很小,真正的分水岭在数据清洗、特征口径和验证方案。你翻看这类获奖论文时最容易踩的误区,是盯着它的最后一版分数,想复现同一个调参结果。可真实差距在数据预处理阶段就已经拉开了:谁把零值处理得更贴合业务,谁把跨周期的特征窗口设置得更合理,谁在训练验证时不泄漏未来信息,谁就赢在了起跑线上。这篇笔记要解决的就是这些问题:从数据口径、特征构造、模型选型到时间序列验证,把订单需求预测做成一条能复现的流水线,并交代参数怎么设、坑在哪里。

2. 把订单需求预测变成可建模问题:数据口径与清洗

订单需求预测这个标题听起来直白,但落地的第一步不是找模型,而是把业务问题翻译成数学问题。你拿到的原始数据往往是一张订单流水表,每行是一次下单记录,包含产品编号、下单日期、下单数量、客户编号等字段。但“需求预测”的预测对象到底是什么,在一开始就要定清楚:是预测每天的订单笔数,还是每天的成交总量,还是未来一周内每个产品的累计需求量?不同的口径会直接改变数据聚合方式、标签取值和模型输入结构。

2.1 预测对象不是订单流水,而是按时间聚合后的需求序列

绝大多数获奖方案的第一步,都是把订单流水按“产品 + 日期”聚合成日需求序列。原因很简单:流水表里同一产品在同一天可能有多条订单,如果直接拿流水做监督学习,模型会把“同一日期下的多条记录”当成多个独立样本,既放大样本量,又让日期特征失去意义。常见的做法是保留两条聚合路径:一条是“日需求总量”,用于整体趋势建模;另一条是“产品日需求量”,用于分产品预测。在之前某模拟项目里,我见过一个团队跳过聚合直接建模,训练集上的 RMSE 很低,换到真实提交时分数崩掉,就是因为样本里充满了重复日期信息,模型记住了订单记录数而不是需求规律。

聚合之后还要处理三类脏数据:缺失、异常、重复。缺失最常见是某些日期完全没有订单记录。要注意,这里不能直接删除,因为无记录可能意味着“零需求”,也可能意味着数据漏采。建议按产品维度先看时间连续性,再决定用零填充还是用前向填充。重复订单的判定也要谨慎,不同来源的数据集里,同一个客户在几分钟内连续补单是常态,这类记录要按业务规则确认,而不是机械地去重。

2.2 数据表里三个容易被忽略但决定预测上限的字段

订单数据表的常见字段有订单编号、产品编号、下单日期、订单数量、客户编号、金额等。有三个字段很容易被忽略,却直接影响预测上限。

第一个是“下单时间”的具体时刻。很多赛题只给日期,但如果有时刻,就能构造“小时级下单偏好”,再按天的分布回退到日粒度特征,可以额外区分工作日与周末的订单结构差异。第二个是“产品类别”。不同类别的生命周期和需求模式完全不同,比如消耗品与耐用品,前者周期性强,后者受促销影响大。聚合特征时要按产品类别分组统计,而不是全表统一处理。第三个是“客户编号”。它能用来构造复购特征,比如该产品最近7天的新客占比、老客复购间隔。如果数据里没有客户编号,也不要硬造,优先用好时间特征和产品自身历史值。

2.3 用 pandas 清洗订单数据:缺失值、异常值与重复订单处理

下面给出一段可复用的清洗代码,假设原始表为 order_data.csv,字段包括 product_id、order_date、quantity。先把流水聚合成日期序列,再处理缺失、异常和重复。

import pandas as pd import numpy asnp # 读取原始订单流水,并统一时间格式 df = pd.read_csv('order_data.csv', parse_dates=['order_date']) # 按产品+日期聚合,同一产品的当日订单量求和 daily = df.groupby(['product_id', 'order_date'])['quantity'] \ .sum().reset_index() # 生成完整日期范围,用于检查缺失日期 all_dates = pd.date_range(daily['order_date'].min(), daily['order_date'].max(), freq='D') # 对每个产品补齐缺失日期,未发生的日期用 NaN 占位 full_index = pd.MultiIndex.from_product( [daily['product_id'].unique(), all_dates], names=['product_id', 'order_date'] ) daily_full = daily.set_index(['product_id', 'order_date']) \ .reindex(full_index).reset_index() # 缺失值按“同产品近7天中位数”填充,而不是用全表均值 daily_full['quantity'] = daily_full.groupby('product_id')['quantity'] \ .transform(lambda x: x.fillna(x.rolling(7, min_periods=1).median()))

这段代码的要点在于填充策略。用同产品近 7 天中位数,能最大程度保留该产品的量级特征;如果改用全表均值,会让低价产品和高价产品的需求水平被拉平,模型后续很难区分不同产品的销售基数。接着做异常值和重复订单处理:

# 用 IQR 识别异常值,但只做 cap,不直接删除 def cap_outliers(s): q1, q3 = s.quantile(0.25), s.quantile(0.75) iqr = q3 - q1 upper = q3 + 3 * iqr return s.clip(upper=upper) daily_full['quantity'] = daily_full.groupby('product_id')['quantity'] \ .transform(cap_outliers) # 如果有明确订单号,按订单号去重; # 如果只有产品+日期+数量,去重逻辑要按业务确认 if 'order_id' in df.columns: df = df.drop_duplicates(subset=['order_id'], keep='first') print(f"去重后剩余订单数: {len(df)}")

异常值处理上,我的选择是用 3 倍 IQR 做向上截断而不是直接删行。原因很实际:在订单数据里,一个陡增的单日销量往往是真实的大促或补货行为,直接删掉会让模型永远学不到“突发峰值”存在的可能。截断保留了“大”的语义,只是限制它不要过分主导损失函数。需要注意,这里的 cap 逻辑是按产品分组做的,因为不同产品的销量量级差异很大,全局截断基本等于没有截断。

3. 特征工程与模型选型:周期、趋势、促销三个抓手

数据清洗完之后,订单需求预测的核心竞争力就转移到特征工程上。做过几轮比赛的人应该有个共同感受:模型决定下限,特征决定上限。在同一个竞赛里,把 LightGBM 换成 XGBoost 通常只带来千分位的提升,但多加一个“去年同星期均值”特征可以把验证集误差往下拉一大截。订单需求预测尤其吃特征,因为它的可学习信号高度集中,来来回回就是周期、趋势、突变三类。

3.1 订单数据里的三种可学习信号:周期、趋势与事件冲击

周期信号是订单需求预测最明显的规律。日数据里通常存在周周期,周一到周五的需求形态和周末完全不同;部分品类还存在月周期,比如办公用品在月初下单量大,个人消费品在月末更活跃。趋势信号相对缓慢,要看产品生命周期或者用户规模变化,比如一个新兴品类可能整体向上爬坡,一个成熟品类则长期横盘。事件冲击是最难学的一类,比如平台大促、天气变化、库存补货节奏,都会造成短期需求脉冲,这类信号往往没有足够的历史样本,需要靠规则特征显式放进模型。

实际构建特征时,我一般会把三组特征分开做,避免混在一起影响可解释性。周期特征用 lags 和星期哑变量,趋势特征用滚动均值和差分,事件冲击尽量找数据里有没有“是否促销”“是否节假日”这类标记字段。如果赛题数据里没有明确标记,退而求其次,把月初、月中、月末、周初、周末做成哑变量,也能捕捉一部分统计规律。

3.2 把时间序列转成监督学习:Lag 特征与滚动统计的窗口怎么设

时间序列预测最常见的套路,是把预测问题改造成回归问题:用过去若干天的值预测下一天的值。这一步的关键是 Lag 特征和滚动窗口的选择。以一个 287 天的历史数据、预测未来 7 天需求的赛题为例,常见做法是构造 lag_1、lag_7、lag_14、lag_28,分别捕捉昨天、上周、上上周和上月的同期水平;滚动窗口则用 7 天均值、14 天均值、28 天均值,再加一个 7 天标准差刻画波动。

# 基于清洗后的 daily_full 构造监督学习特征 feature_df = daily_full.copy() feature_df = feature_df.sort_values(['product_id', 'order_date']) # 滞后特征:预测第 t 天,用第 t-1、t-7、t-14、t-28 天的需求 for lag in [1, 7, 14, 28]: feature_df[f'lag_{lag}'] = feature_df.groupby('product_id')['quantity'] \ .shift(lag) # 滚动统计特征:窗口为 7 天和 28 天的均值、标准差 for window in [7, 28]: roll = feature_df.groupby('product_id')['quantity'] \ .rolling(window, min_periods=1) feature_df[f'roll_mean_{window}'] = roll.mean().reset_index(level=0, drop=True) feature_df[f'roll_std_{window}'] = roll.std().reset_index(level=0, drop=True) # 日历特征 feature_df['weekday'] = feature_df['order_date'].dt.weekday feature_df['is_month_start'] = feature_df['order_date'].dt.is_month_start feature_df['is_month_end'] = feature_df['order_date'].dt.is_month_end feature_df['month'] = feature_df['order_date'].dt.month

关键参数是 Lag 的跨度组合。如果预测周期是 7 天,lag_1 到 lag_7 必须同时存在,因为模型需要学习一周内从第 1 天到第 7 天的需求形态;如果不加 lag_7 而只加 lag_1,模型很难区分周一和周日。滚动窗口不是越大越好,窗口太大意味着特征越平滑,对近期突变的响应越慢;窗口太小则噪声太多。7 和 28 是我常用的组合,分别对应短周期和长周期,两个窗口叠加,可以同时保留近期波动与中期趋势。

需要注意一个细节:加完 Lag 特征后,前若干行会有 NaN,因为最早的日期没有足够的历史数据。这些行对训练没有帮助,但也不要直接全部丢掉,而是要在分组内检查每个产品剩余的样本长度,避免某些产品因为 lag_28 导致训练样本严重缩减。实际经验是,如果产品历史不足 28 天,可以只保留 lag_1 和 lag_7,把它单独归入“冷启动”样本。

3.3 模型选型:为什么一等奖方案普遍不押注单一模型

订单需求预测的合适基模型,业界和竞赛圈基本收敛到了梯度提升树和 LSTM 两类。梯度提升树以 LightGBM 和 XGBoost 为代表,优点是表格特征处理方便、对缺失值有原生支持、训练速度快,特别适合 Lag 特征丰富的表结构。LSTM 的优势在于直接建模长依赖序列,但缺点是数据量小的时候容易过拟合,且特征工程与树模型完全不同的构造思路。

很多获奖方案的做法不是“二选一”,而是树模型加时序模型再融合。常见的是 LightGBM 作为主力,LSTM 或 Prophet 作为辅助,最后按验证集表现做加权平均。这里有个心态问题:不要为了炫技把模型堆得太复杂。订单预测的样本量通常只有几万行,LSTM 在这么小的样本上并不稳定,融合太多模型反而容易把噪声带进去。一个靠谱的基线是:先只跑 LightGBM,把特征和验证流程梳理通,再验证后再加第二个模型,观察分数是否真的提升。

我记得某次模拟项目上,A 同学在 LightGBM 之外强加了一个三层 LSTM,验证集分数没有提升反而掉了 2%,原因是 LSTM 对近期波动的过拟合干扰了加权融合。最后还是回到单模型加后处理。所以选型的结论是:先树模型打底,再按需加融合,不要本末倒置。

4. 训练与验证:时间序列交叉验证避免虚假高分

订单需求预测和普通回归任务最大的区别,是样本之间存在严格的时间依赖。很多人把数据集随机打乱,然后按样本量做 K 折交叉验证,得到的验证分数往往非常漂亮,但真实提交时分数一落千丈。这不是模型不稳定,而是验证策略本身泄漏了未来信息。

4.1 普通 K 折在订单预测里为什么是作弊

标准的 K 折交叉验证假设样本独立同分布,随机抽 20% 当验证集没问题。但订单数据是按时间排列的,第 100 天的订单和第 101 天的订单天然相关。如果随机划分,训练集里会混入验证集日期之后的记录,模型等于提前看到了“未来”的需求走势,验证分数自然偏高。这类误差在订单预测里尤其明显,因为强周期特征会让模型只记住时间位置而不是需求模式。

要验证这个问题,一个简单的实验是把数据按时间排序,手动用最后 20% 当验证集,和随机 K 折的结果对比,你会发现随机 K 折的分数普遍好很多。这不是模型进步,是信息泄漏。真正的方案必须保证验证集的时间永远在训练集之后。这既是赛题要求,也是产品上线后真实环境的模拟。

4.2 实现时间序列交叉验证:按时间切分的代码与折数选择

时间序列交叉验证也叫滚动式验证。以 287 天数据为例,可以切成 4 折:第 1 折用第 1 到 180 天训练,预测第 181 到 210 天;第 2 折用第 1 到 210 天训练,预测第 211 到 240 天;依此类推,最后一折用全部历史预测末段日期。折数不要设太多,每折之间的间隔要大于最大 Lag 值,否则验证集的第一天仍然可能和训练集的最后一天产生信息重叠。

from sklearn.metrics import mean_squared_error import lightgbm as lgb # feature_df 包含所有特征,label 是 quantity # 按产品排序后,时间索引可以按全局日期切分 test_starts = [180, 210, 240, 270] # 每折验证集起始日索引 valid_days = 30 # 每折验证 30 天 # 只保留没有 NaN 的行,注意过滤掉 lag 特征带来的冷启动缺失 train_df = feature_df.dropna(subset=[f'lag_{l}' for l in [1, 7, 14, 28]]) features = [c for c in train_df.columns if c not in ['product_id', 'order_date', 'quantity']] x = train_df[features] y = train_df['quantity'] for idx, start in enumerate(test_starts): # 按日期顺序划分训练与验证集 cutoff_date = train_df['order_date'].min() + pd.Timedelta(days=start) train_mask = train_df['order_date'] < cutoff_date valid_mask = (train_df['order_date'] >= cutoff_date) & \ (train_df['order_date'] < cutoff_date + pd.Timedelta(days=valid_days)) x_train, x_valid = x[train_mask], x[valid_mask] y_train, y_valid = y[train_mask], y[valid_mask] model = lgb.LGBMRegressor( n_estimators=500, learning_rate=0.05, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(x_train, y_train, eval_set=[(x_valid, y_valid)], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)]) pred = model.predict(x_valid) rmse = mean_squared_error(y_valid, pred, squared=False) print(f"Fold {idx+1}: RMSE = {rmse:.4f}")

这段代码的核心逻辑是按全局日期切分,而不是按行数比例切分。LightGBM 的参数里,n_estimators 和 early_stopping 是配合使用的,early_stopping 设为 50 表示如果连续 50 轮验证集分数不降就停止,能防止树模型过度迭代。num_leaves 控制在 31 附近,避免单棵树过深。subsample 和 colsample_bytree 设为 0.8,相当于同时做了样本采样和特征采样,对抑制过拟合很有效。

折数选择上,4 折是我在 6 个月数据量下的默认值。如果数据量很大,可以加到 5 折;如果数据量小,3 折更稳妥,因为训练集太薄会导致每折分数波动巨大。另一个容易忽略的点是,每折验证集的长度要大于你要预测的周期。如果比赛要求预测未来 7 天,验证集至少 7 天,我用 30 天是为了让指标更稳定,也能观察模型在不同时间段的误差差异。

4.3 评估指标:RMSE 与分位数损失的取舍

订单需求预测常用指标有 RMSE、MAE 和分位数损失。RMSE 对大误差非常敏感,一个大的预测偏差会被平方放大,所以它更像是在惩罚那些把大促需求预测成普通水平的错误。MAE 则更稳健,适合对每个产品的误差做平均比较。在很多获奖方案里,最终评分不是单一指标,而是分位数损失,即预测的不是一个点而是多个分位数,比如 10%、50%、90% 分位数。这种设定更贴近真实业务:仓库备货要的是满足一定服务水平的需求上限,而不是一个单点均值。

如果你最后只看 RMSE,模型会倾向输出一个保守的中间值,这会导致需求高峰期系统性低估。为了避免这个问题,我通常把预测目标先做 log1p 变换,让模型在取对数后的空间里拟合,再逆变换回来。这样大需求的绝对误差被压缩,模型不会过分迁就少数高量级产品。逆变换时要注意,不能直接把 expm1 用在所有预测值上,而是先确认训练时的变换方式一致,否则数值偏移会失真。

# 使用 log1p 变换缓解需求量级差异 from numpy import log1p, expm1 # 训练前 y_train_log = log1p(y_train) # 训练和预测后 pred_original = expm1(model.predict(x_valid))

加入 log1p 变换后,RMSE 的绝对值会变大,因为还原到了原始量纲,但模型在跨产品的平均表现上通常更好。关键是验证集指标要始终使用原始量纲计算,不要直接比较 log 空间的误差,否则复现时容易被自己误导。

5. 避坑指南:订单需求预测的五个翻车现场

订单需求预测的坑非常多,而且很多坑要在提交之后才暴露。这里整理五条我认为最常见的踩坑记录,每条都按“现象、原因、解决”来写,走了一遍之后回头再看,你会发现在这些地方省下的时间都值得。

5.1 训练集和验证集存在时间重叠,模型分数虚高

现象:随机 K 折验证分数很低,信心满满地提交,结果线上排名远低于预期。原因:随机划分泄漏了未来信息,模型看到了验证集同期的数据。解决:一律使用按时间切分的滚动验证,且每折验证集起始日要大于所有 Lag 特征的最大跨度。另一个隐蔽的重叠来源是滚动特征使用了 min_periods=1,这会让窗口开头几天用到未来数据。比如落在 7 月 10 日的样本,如果它的滚动窗口横跨了训练和验证边界,就产生了轻微重叠。解决方法是所有特征都只基于历史窗口生成,保证任何一行的特征里都不含当日之后的信息。

5.2 缺失值用全局均值填充,导致产品需求基数被平均

现象:预测结果所有产品的需求水平看起来差不多,高销量产品明显被拉低,低销量产品被抬高。原因:清洗阶段用全表均值填充缺失日期,不同产品的量级差异被抹平。解决:按产品分组做 fillna,且优先用该产品近 7 天中位数。如果该产品的历史全部为空,再用全表同品类产品的均值兜底。这一步在复现时经常被跳过,却是分数差异的主要来源。

5.3 Lag 窗口跨度过短,模型完全学不到周期形态

现象:训练集误差不错,但验证第一天误差特别大,且误差按 7 天为周期震荡。原因:只用了 lag_1 和 lag_2,模型把昨天的需求当成最强的预测因子,遇到周一预测周一,只能拿上周日的值硬凑,周期信息没有进入特征。解决:把 lag_7 和 lag_14 加进特征集,同时补上 weekday 哑变量。一般建议最大 lag 至少覆盖一个完整周期。竞赛里常见的最大跨度是 28 天,因为 28 天可以覆盖一个月的同星期对照。

5.4 把所有零值当成缺失值或异常值

现象:清洗后数据里的零值全被删掉,模型训练样本量骤减。原因:很多人在预处理时看到 quantity 等于 0 的订单,认为这是无效记录,直接过滤掉。但零值可能是真实情况,比如某些产品当天确实无人下单,或者发生了退款导致净需求为零。把零值删掉,模型学到的是“有订单的日子”的分布,而不是“每一天”的需求分布。解决:先区分“缺失”和“零值”。缺失用中位数填充,零值保留下来作为有效训练样本。

5.5 提交预测结果时产品 ID 或日期对不齐

现象:本地验证分数很高,提交报错或者分数极低。原因:预测时生成了未来日期序列,但生成时用的是全表产品列表,有些产品在最后 7 天没有历史记录就被漏掉,或者日期索引偏移了一天。解决:写提交脚本时,显式构建“产品 × 未来日期”的完整笛卡尔积,不要依赖测试集的行数。拼装好之后,再和官方提交模板做一次行数校验,确保行数一致后,再检查每个产品的预测日期是否都包含完整预测窗口。

6. 从一等奖方案里能抄走的三个进阶习惯

看完一份获奖论文,真正能带走的不只是那几个模型参数,而是它背后的一整套工作习惯。第一个习惯是坚持写“验证日志”。每跑一组特征或改一个参数,就把验证集 RMSE、训练集 RMSE、特征列表、参数组合记录在表格里。看得多了,你会发现哪些改动对分数是真实有效,哪些只是噪声波动。这个习惯在竞赛后期尤其重要,能让你避免反复在同一组参数上浪费时间。

第二个习惯是预测之后做按产品维度的后处理。树模型对所有产品的拟合是全局的,但不同产品有不同生命周期和销售基数,全局调参无法照顾到每个个体。常见做法是把预测值按产品最近 7 天的均值做比例缩放。某个产品近期销量整体上升,就把它的预测整体上调;近期销量回落,就相应下调。这个手段不一定在比赛里每次都有正收益,但在真实业务里非常实用。

第三个习惯是保留一个“不能触碰”的盲测集。把最末一段时间的数据彻底锁起来,不参与任何特征选择和参数调整,只在最终评估时使用。这样做的好处相当于真正模拟了线上环境,你可以清楚看到自己的模型在完全未见过的数据上表现如何。配一套这个盲测流程,比任何交叉验证都更能反映方案的实际水平。

我自己的习惯是,在敲定最终模型之前,先跑一遍全流程的基线,再从盲测集误差最大的那几个产品反推特征缺陷。很多时候,解决一个产品群体的误差,比优化整体 RMSE 更有价值。这套思路谈不上多么高级,但对订单需求预测这样的竞赛场景足够可靠——先把数据和验证做扎实,再在模型的细节上精雕细琢。希望这篇笔记能帮你把一份获奖论文真正落地成自己的方案,少走几趟弯路。

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

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

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

立即咨询