资讯详情

订单需求预测避坑:时序特征、数据泄漏与模型选型

📅 2026/10/27 4:19:15 | 华诺云谱 👁 阅读
订单需求预测避坑:时序特征、数据泄漏与模型选型
简介面向外卖订单需求预测场景的机器学习项目包基于Swiggy Hackathon 2018赛题构建适合具备Python与回归建模基础、希望完整体验从数据准备到结果可视化的学习者。压缩包共75个文件约4.63MB以Python脚本、JSON配置与预测结果、PNG/SVG图表及前端JS资源为主兼有CSV订单数据、PDF方案说明和少量页面模板涵盖建模代码、数据与Web演示三部分。已有414人学习下载。内容围绕订单量周期性、趋势与异常值识别及时间、地理特征工程展开提供K近邻回归和随机森林回归两套实现并包含训练测试划分、MSE/MAE/R²评估、参数调优与交叉验证思路配套预测输出、Node/Vue可视化接口与结果图表可对照理解从需求预测到业务呈现的完整链路。目录结构便于按数据准备、模型脚本和可视化模块检索适合课程项目复现、赛题复盘与回归建模练习。1. 订单需求预测的第一课模型分数越漂亮越要怀疑它偷看了未来订单需求预测最反直觉的一点是把历史订单直接喂给一个回归模型跑出来的往往不是预测而是对历史的复读。验证集 R² 到 0.95 以上并不稀奇稀奇的是这个模型上线之后该加人的时段没人、闲着的时段堆了一堆运力。问题几乎从不出在算法选型上KNN 也好、随机森林也好都是被冤枉的真正出错的地方在样本怎么造、特征怎么切、指标怎么算这三件比调参重要一个数量级的事上。搞懂这套机制的底层逻辑你才会明白为什么同一份订单数据上一个近邻模型和一个树集成能差出两倍误差也才会知道那些漂亮分数到底偷了哪一段未来。这篇内容拆的就是这条链路上每一步的设计动机和实现细节。2. 样本构造订单流水为什么不能直接喂进回归模型原始订单表是事件流一行是一笔订单带时间戳、区域、金额、状态。它在数据库里长得像样本但在机器学习语义里完全不是——每一行的标签是订单量本身等于 1特征只有时间和地点。直接拿这种表训练模型学到的是订单通常发生在有订单的时刻一句正确的废话。需求预测真正要的是时空网格上的强度某个区域、某个时间桶里一共发生了多少需求。这个从事件流到面板数据的转换决定了后面所有模型的天花板。2.1 时间窗口聚合粒度选错后面全白干聚合粒度是第一个必须拍板的参数。按天聚合样本量大、零值少、趋势平稳但预测结果没法指导小时级排班按 15 分钟聚合颗粒度够细可大多数区域×15 分钟格子是 0序列变成一堆稀疏尖峰任何回归模型都会退化成预测接近零。实践中常见的折中是小时粒度既能对上运力调度的排班周期又不至于让零值占比超过七成。聚合时还有两个容易被忽略的动作。一是补齐空缺时间桶。真实数据里没有订单的小时不会产生行如果直接 groupby模型看到的全是有需求的时刻先验被系统性抬高预测普遍偏大。必须用完整的多重索引 reindex 一遍把缺失填 0让模型见到零需求这个真实存在的状态。二是明确目标变量的口径是订单笔数、还是配送单量、还是 GMV。笔数最稳GMV 受客单价波动干扰会把促销带来的价格变化误读成需求变化。import pandas as pd # 1) 逐笔订单 - 「区域 × 小时」的需求强度面板 orders[order_hour] orders[created_at].dt.floor(h) panel ( orders.groupby([zone_id, order_hour]) .agg(demand(order_id, count)) # 目标变量该小时该区域的订单笔数 .reset_index() ) # 2) 补齐没有订单的小时否则「零需求」整段丢失模型先验被抬高 full_index pd.MultiIndex.from_product( [panel[zone_id].unique(), pd.date_range(startstart_date, endend_date, freqh)], names[zone_id, order_hour], ) panel ( panel.set_index([zone_id, order_hour]) .reindex(full_index, fill_value0) .reset_index() .sort_values([zone_id, order_hour]) )这段代码的关键不在 groupby而在 reindex。fill_value0不是随手写的默认值它代表这个格子确实没有需求这一业务事实如果换成前向填充等于告诉模型没订单的时候需求延续上一小时会在夜间低谷区制造出一条虚假的平滑曲线。另外dt.floor(h)而不是dt.hour是因为后者会丢掉日期把不同天的同一小时混成一桶。2.2 特征工程的三个层次滞后、滑动窗口与日历编码面板建好之后特征大致分三层。第一层是滞后项lag_1、lag_24、lag_168分别对应上一小时、昨天同一小时、上周同一小时。这三个滞后不是随便挑的它们对应三种不同的周期性短程惯性、日周期、周周期。外卖和即时配送的周周期极其明显周末和工作日的形态几乎换了一套分布lag_168往往是单特征里最强的那个。第二层是滑动统计比如过去 3 小时、24 小时的均值和标准差。均值刻画近期水位标准差刻画波动性——一个区域如果历史波动大模型对它的预测本该更保守而标准正态的回归模型没有这个自觉得靠特征把波动信息喂进去。第三层是日历编码。小时、星期几、是否周末、是否发薪日附近、是否大促这些离散量怎么编码直接决定模型能不能用。小时如果用 0 到 23 的整数模型会认为 23 点和 0 点相距最远而实际上它俩是挨着的这是典型的编码错误正确做法是 sin/cos 双通道映射到圆周上。同理星期几也建议做周期编码。这一层最容易翻车的地方是泄漏。所有时序特征必须先 shift 再 rolling顺序反了就是用当前时刻的信息预测当前时刻。def make_features(panel, lags(1, 2, 24, 168), windows(3, 24)): panel panel.sort_values([zone_id, order_hour]).copy() g panel.groupby(zone_id)[demand] # 滞后特征显式指定 shift 步数步数 提前多少小时预测 for lag in lags: panel[flag_{lag}] g.shift(lag) # 滑动统计必须 shift(1) 之后再 rolling否则窗口尾部含当前点 泄漏 for w in windows: panel[froll_mean_{w}] g.shift(1).rolling(w).mean() panel[froll_std_{w}] g.shift(1).rolling(w).std() # 周期编码小时映射到单位圆23 点和 0 点距离才不会被拉爆 hour panel[order_hour].dt.hour panel[hour_sin] np.sin(2 * np.pi * hour / 24) panel[hour_cos] np.cos(2 * np.pi * hour / 24) dow panel[order_hour].dt.dayofweek panel[dow_sin] np.sin(2 * np.pi * dow / 7) panel[dow_cos] np.cos(2 * np.pi * dow / 7) # 冷启动期前 168 小时没有完整滞后直接丢弃别用 0 填 return panel.dropna(subset[flag_{max(lags)}]).reset_index(dropTrue)g.shift(1).rolling(w)这一行的顺序是有讲究的先整体下移一格再对窗口求统计保证窗口里的每一个点都严格早于当前时刻。写完shift(1)再rolling和写完rolling再shift(1)代码只差几个字符验证集分数能差出 30%这是血泪经验级别的细节。最后那个dropna也不能省冷启动期的滞后是 NaN用 0 填充等于告诉模型上周这个时候一单没有会在训练集里制造一批假样本。2.3 时间切分与评估指标随机划分是自欺欺人时序数据做随机 shuffle 划分等于让模型在训练时见过未来。相邻小时的样本高度自相关训练集里留着 14 点和 16 点验证集抽到 15 点模型几乎是查表答出来的。所以划分必须按时间切前 80% 做训练后 20% 做验证中间还要留一个 gap长度等于预测提前步数否则训练集最后几个点的标签会跨进验证集窗口。指标同样要挑。RMSE 对零值和尖峰极度敏感一个暴雨天的爆单能把整体 RMSE 拉高一大截让你误以为模型全线崩了。需求预测更常用的是 WAPE加权绝对百分比误差分母是总需求量而非单点需求天然抗零值def wape(y_true, y_pred): # 分母用总需求零值样本不会导致除零也不会被无限放大 return np.abs(y_true - y_pred).sum() / np.abs(y_true).sum()如果预测结果要用来定安全库存或运力冗余光有点预测不够得看分位数。随机森林天然能给分位数把每棵树的叶子输出收集起来取 90 分位就得到一个偏保守的预测值。这个特性在备货场景里比均值预测有用得多也是树模型相比 KNN 的一个隐性优势。3. KNN 回归的物理直觉用「相似的过去」外推什么时候会失效KNN 回归的假设朴素到近乎可爱如果当前时刻的特征向量和历史上某些时刻长得很像那这些时刻的需求量取个平均就是当前的预测。它没有任何参数要学训练阶段只是把数据存起来预测阶段现场找邻居。这个性质在需求预测里既是优点也是致命伤——优点是它天然适配分区域建模每个区域自己找自己的相似历史伤是它对特征尺度、维度、以及相似的定义极其敏感稍微没伺候好就退化成一个随机数生成器。3.1 距离度量不做标准化KNN 直接翻车欧氏距离是把各维度的差值平方求和。如果lag_1的量级在 0 到 200 之间而hour_sin在 -1 到 1 之间那么前者一项就能吃掉距离的绝大部分后者的贡献小到可以忽略。模型表面上用了十个特征实际上只用了那一个数值最大的。所以 KNN 必须放进 Pipeline把标准化和模型绑定在一起而不是先手动 transform 再 fit——后者在交叉验证时会造成标准化参数跨折泄漏。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsRegressor pipe Pipeline([ (scaler, StandardScaler()), # 必须在 Pipeline 里跟着每折重算均值方差 (knn, KNeighborsRegressor( n_neighbors25, weightsdistance, # 近邻权重按 1/d 加权 metriceuclidean, n_jobs-1, )), ])weightsdistance让距离近的邻居说话更响逻辑上很合理但有个坑如果某个邻居和当前点的距离恰好是 0重复样本时序数据里很常见权重会变成无穷大。sklearn 内部对这种情况有处理把零距离邻居的权重设为极大值并只保留它们效果等价于直接抄最近的那个点。遇到重复样本多的数据集要么先去重要么加一个极小的 epsilon 到距离上。3.2 k 值与距离加权偏差-方差在需求数据上的具体形状k 是 KNN 唯一的超参也是它全部的表达能力所在。k1 时模型完全跟着噪声走训练误差为零验证误差爆炸k 增大到接近样本量时预测收敛到全局均值偏差拉满。需求数据的特点是信噪比低、自相关强所以最优 k 通常偏大10 到 50 这个区间比较常见。可以用一条扫描曲线把这件事看清楚而不是凭感觉拍from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5, gaphorizon) # gap 预测步长防止窗口重叠 scores {} for k in [3, 5, 10, 20, 30, 50, 80, 120]: pipe.set_params(knn__n_neighborsk) fold_wape [] for tr, va in tscv.split(X): pipe.fit(X.iloc[tr], y.iloc[tr]) fold_wape.append(wape(y.iloc[va], pipe.predict(X.iloc[va]))) scores[k] np.mean(fold_wape)gaphorizon这个参数很多人不知道它的作用是在训练折和验证折之间挖掉 horizon 个样本。如果你预测的是未来 6 小时的需求而训练集末尾和验证集开头紧挨着那验证集的前几个点其实被训练集的最后几个滞后特征看见了分数会虚高。曲线通常呈现一个浅 U 形U 底附近就是合理区间取偏大一点更稳。3.3 维度灾难独热编码之后 KNN 为什么失效需求数据里有一类特征特别诱人区域 ID。几百个区域独热编码之后变成几百维的 0/1 向量。对树模型来说这没什么对 KNN 来说是灭顶之灾——高维空间里所有点对的距离会趋于相等最近邻和最远邻的差距被抹平相似这个概念本身失效。这就是维度灾难在工程上的具体表现不是维度高了算得慢而是距离度量失去了区分度。一个经验判据是当特征维度逼近样本量的平方根时KNN 的表现就开始明显下滑。几百维独热加几十维时序特征维度轻松破千此时 KNN 的预测结果和随机取历史均值没有本质区别。合理的替代方案是用低频类别合并、目标编码必须用 out-of-fold 方式否则泄漏严重或者干脆换树模型。树模型对单调变换不敏感不需要标准化也不怕维度高这是它和 KNN 最根本的分野——KNN 依赖距离这个几何概念树模型依赖分裂这个序关系。4. 随机森林回归两重随机性如何把订单噪声平均掉如果说 KNN 是在特征空间里找邻居随机森林就是在做另一件事用一堆各自有偏的树通过平均把方差压下去。订单需求数据天生噪声大——一场雨、一次临时交通管制、一个平台活动都能让某个区域某个小时的需求偏离均值好几倍。单棵树会把这些噪声当成规律记下来而一群树各自记下的噪声不一样平均之后相互抵消剩下的才是真正的信号。4.1 回归树在拟合什么叶子节点的均值单棵回归树的训练过程就是不断切分特征空间每次挑一个特征和一个阈值让切分后两边的 MSE 下降最多直到叶子节点样本数低于阈值或深度到顶。预测时新样本落到哪个叶子就输出那个叶子里所有训练样本的均值。这个机制带来一个必须知道的限制树只能输出训练集中出现过的叶子均值它的预测值永远落在训练标签的最小值和最大值之间。也就是说随机森林无法外推。如果你的区域处于快速增长期未来需求必然突破历史峰值森林会把它死死按在历史最高点系统性低估。这不是 bug是阶梯函数的数学性质。工程上的解法有两个方向。一是对目标做差分预测增长率而不是绝对值再把增量加回去二是显式剥离趋势项对去趋势后的残差建模预测完再把趋势加回来。直接上森林去预测一个单调上升的序列是新手最常见的踩坑方式。import numpy as np from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators400, max_features0.4, # 每次分裂只考虑 40% 特征压低树间相关性 min_samples_leaf5, # 叶子至少 5 个样本抑制单点噪声被记住 oob_scoreTrue, # 用袋外样本估误差不占用验证集 n_jobs-1, random_state42, ) rf.fit(X_train, y_train)min_samples_leaf在需求预测里比max_depth更值得调。原因是需求数据的局部噪声极强叶子样本太少时一棵树会把某个周三晚上下雨这种一次性事件固化成一条规则。把叶子样本数抬到 5 到 20等于给模型加了一层强平滑泛化误差通常能降一大截。4.2 Bootstrap 与特征子采样方差是怎么被平均掉的随机森林的两重随机性各司其职。第一重是 Bootstrap每棵树用有放回抽样得到的不同子集训练让每棵树看到的历史略有差异第二重是特征子采样每次分裂只在随机选出的部分特征里找最优切分点让树的结构产生分化。这两重随机的目的都是降低树与树之间的相关性。集成模型的方差公式大致是Var(ensemble) ρ·σ² (1 - ρ)·σ² / B其中 σ² 是单棵树的方差B 是树的数量ρ 是树之间的平均相关系数。这个式子说明了一件反直觉的事当 B 已经足够大时继续加树几乎不降方差因为第一项 ρ·σ² 是地板。真正有效的是把 ρ 压下去也就是调小max_features。这就是为什么n_estimators从 100 加到 1000 收益甚微而max_features从 1.0 降到 0.3 常常有明显提升。不过 ρ 也不能无限压。max_features太小每棵树都变得很弱偏差上升需要更多树来补偿。经验上回归任务取特征数的 1/3 到 1/2 是个不错的起点。4.3 OOB 与特征重要性没有独立验证集时的透视手段时序数据本来就紧俏切出一块验证集肉疼。oob_scoreTrue提供了一个几乎免费的替代每棵树训练时约有 36.8% 的样本没被抽到这些袋外样本可以当作该树的天然验证集汇总起来就是一个近似无偏的误差估计。from sklearn.inspection import permutation_importance # 不纯度重要性有偏偏向高基数特征用置换重要性交叉验证 perm permutation_importance( rf, X_val, y_val, n_repeats10, scoringneg_mean_absolute_error, random_state42, ) importance pd.Series(perm.importances_mean, indexX_val.columns).sort_values(ascendingFalse)这里必须提醒一句rf.feature_importances_用的是不纯度下降它对高基数特征有系统性偏好连续型特征往往被高估。真正靠谱的是置换重要性——把某一列随机打乱看误差涨多少涨得多说明这列重要。两种方法给出的排序经常相差很大以置换重要性为准。还有一个更隐蔽的陷阱时间戳类特征。如果把原始时间戳或其派生的大数值比如距 1970 年的秒数放进特征树会用它当代理变量间接拟合出趋势和季节性重要性排到第一。看起来模型学到了东西实际上它在偷偷做外推一旦时间超出训练范围就完全失控。所有时间信息都应该被拆成周期编码后使用原始时间戳一律不进特征。5. 排查手册五个让需求预测悄悄失真的现场模型跑通不难难的是判断它到底是学到了规律还是记住了噪声。下面这几条是实际项目里反复出现的失真现场共同点是验证集分数很好看上线之后一地鸡毛。5.1 现象验证集 WAPE 低于 10%上线后预测值整体偏高原因几乎可以锁定为泄漏。最常见的是滑动窗口没有先 shiftrolling(24).mean()的窗口尾部包含了当前时刻的需求等于把标签喂进了特征。其次是标准化或目标编码在划分之前就全量 fit 了一遍验证集的统计量污染了训练过程。解决方式是三条硬规矩所有时序特征先 shift 再 rolling所有预处理放进 Pipeline目标编码必须用 out-of-fold 生成。改完这三处分数通常会从 0.1 掉到 0.25 左右——这个更难看的分才是真的。5.2 现象预测值大量重复或者全部接近某个常数原因多半在目标变量本身。如果按 15 分钟聚合且零值占九成模型会学到一个最优策略是永远预测接近零因为这样 MSE 最小。或者目标做过 log 变换但预测后忘了 expm1 还原输出停在对数尺度上。解决方式是先看目标分布零值占比超过 70% 就该考虑换粒度或者改成两阶段建模——先分类判断这个格子有没有需求再对非零部分回归。log 变换一定要配对写还原函数别靠记忆。5.3 现象工作日预测很准周末和节假日系统性偏低原因是分布漂移。训练集里周末样本占比小树在分裂时为了整体 MSE 最优会把周末的样本硬塞进工作日的叶子里输出一个折中值。节假日更极端一年只有那么几天模型根本没见过。解决方式是给日历特征加权把是否周末是否节假日前后做成显式布尔列而不是让模型从星期几里自己推同时对节假日样本做过采样。如果某个节日每年形态不同别指望模型学会直接人工规则覆盖更可靠。5.4 现象KNN 的预测结果跳动剧烈相邻小时预测值能差好几倍原因是标准化缺失或维度爆炸。特征量级差异让距离被单一特征垄断或者独热编码把维度顶到几百维最近邻失去了区分度实际取到的是随机几个历史点。解决方式是强制 Pipeline 里带 StandardScaler检查一遍各特征的标准差量级把独热换成低频合并或嵌入如果维度还是降不下来就承认 KNN 不适合这份数据换树模型。硬调 k 值救不了维度灾难。5.5 现象特征重要性第一名是一个看起来毫无业务含义的列原因是代理特征。比如zone_id编码成了一个连续整数而区域恰好是按入驻时间排序的这个整数就偷偷携带了时间信息再比如行号、索引列、原始时间戳都能被树用来拟合趋势。模型没错是特征造错了。解决方式是把重要性表当成审计清单逐条问这个特征在业务上怎么解释。解释不通的列直接删掉再看分数掉多少。掉得越多说明之前的分越假。6. 选型取舍KNN 与随机森林各自守得住的边界把这两种模型放在一起比较最有价值的不是谁分数高而是它们对数据形态的假设完全不同选错假设比调错参数严重得多。维度KNN 回归随机森林回归特征缩放必须标准化否则距离失真不需要分裂只看序关系高维稀疏极易失效距离趋同表现稳定可处理上百维外推能力弱预测落在邻居范围内无严格落在训练标签区间内训练成本近乎为零较高但可并行预测成本高需扫描全部样本低树深对数级可解释性可指认最近的几个历史时刻可看分裂路径与重要性排序适合场景小样本、低维、局部模式强中大样本、混合特征、噪声大从这张表能读出一个结论KNN 的优势区间其实很窄——样本量不大、特征维度低、且业务上确实存在相似时刻这种局部结构。它最大的价值是可解释性你能直接告诉业务方这次预测是参考了上周三和上上周三同样的时段这种解释力在跨团队沟通时非常好用。而一旦特征维度上去、类别变量变多KNN 就会静悄悄地失效且失效得很隐蔽——它不会报错只会给你一堆看起来合理的数字。随机森林的边界在另一头。它对特征工程的要求低混合类型、非线性、交互项都能自己处理特征重要性还能反过来指导业务。但它的两个硬伤必须提前想清楚一是不能外推业务处于增长期时要么去趋势要么换模型二是模型体积大几百棵树落盘动辄几十兆端侧部署时得做树剪枝或者蒸馏。至于怎么验证自己选对了我的习惯是做一次朴素基线对照。先算一个最简单的基线——用lag_168上周同一时段直接当预测值测出 WAPE。任何模型如果打不过这个基线说明它连周周期都没学会问题出在特征而不是算法。这个基线只用了三个字符的代码却能挡掉一大半无效调参。第二步是分区域看误差分布如果误差集中在某几类区域那多半是特征对该类区域失效而不是全局模型不行分桶看误差比看总分有用得多。还有一个经常被忽略的取舍是预测目标本身。点预测均值适合做趋势分析分位数预测适合做运力冗余分类预测有无需求适合做资源投放决策。同一份订单数据目标定义不同最优模型可能完全不同——KNN 在分位数场景下几乎没法用而随机森林只要收集每棵树的叶子输出就能给出任意分位。从那以后我每次接需求预测类的问题都会先把朴素基线 时间切分 WAPE这三件套跑一遍跑不通就不碰模型。这个习惯救过我很多次也让我少交了不少学费。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑