做时间序列预测的朋友几乎都经历过这样的场景模型A和模型B在测试集上一跑B的MAE比A小了那么一点点你正要写“B显著优于A”却被老板或审稿人问住了——“小多少算真的小”如果你的数据是一段带噪声的时间序列验证集上的误差本身也是随机变量两次不同的测试时间段结果可能完全反过来。Diebold-Mariano检验DM检验就是专门处理这个问题的统计工具它比较两个模型在整个预测期间的损失差异判断这种差异在统计上是否显著而不是只看验证集上谁的数字更好看。1995年Diebold和Mariano在Journal of Business Economic Statistics上发表了这个方法随后成为宏观经济预测、金融时间序列、负荷预测、气象预报等场景里模型对比的标准动作。如果你平时做预测模型选型、写论文、做竞品模型对比这篇文章可以让你直接上手这个检验。1. Diebold-Mariano检验到底解决什么问题1.1 模型比较的永恒难题我见过太多团队在模型选型时用的还是“拍脑袋式”的方法同一个测试集跑完所有候选模型挑误差最小的那个上线。这种做法最大的问题在于它完全忽略了评估结果本身的随机性。你得到的那组验证集误差只是所有可能数据场景下的一个样本实现。换个时间窗口误差排名可能就变了。举个例子。假设真实销量过程带有随机噪声模型A在某段测试集上MSE是1.21模型B是1.08看起来B更好。但如果你把测试窗口往后挪一个月可能就变成A是1.15B是1.19。原因很简单误差序列是随机的样本均值有抽样误差我们不能靠一次样本实现下结论。这就像两家餐厅在美食App上的评分4.5分和4.4分的差距很可能只是最近十个点评的随机波动并不能说明一家真的更好。所以做模型比较需要一套正规的统计检验框架把“误差差值的均值”放到显著性检验的标尺下量一量。DM检验正是为这个需求设计的。1.2 DM检验的核心逻辑看损失差的均值DM检验的思路非常直白。假设你在同一个测试期内用两个模型分别做预测得到两组误差序列模型1的预测误差e1_t模型2的预测误差e2_t常见的损失函数是平方误差或绝对误差那么每个时间点 t 都可以算出一个损失差d_t g(e1_t) - g(e2_t)其中 g 是你选定的损失函数。如果两个模型精度相同整个预测期内损失差的期望值应该是0如果一个模型更好那么 d_t 的期望应该显著偏离0。于是DM检验的原假设写出来就是H0: E[d_t] 0备择假设是 E[d_t] ≠ 0双侧或只关心某个方向单侧。剩下的工作就很简单计算 d_t 的样本均值再判断这个均值相对0的偏离是否大到不能解释为随机波动。注意DM检验不关心某个时间点上谁对谁错它关心的是整个预测期间系统性的精度差异。这是它和“逐点比较命中率”最本质的区别。1.3 为什么不能简单用配对t检验看到这里你可能会问这不就是配对t检验吗还真不是。配对t检验要求观测之间相互独立但时间序列的预测误差几乎不可能满足这个假设。误差通常是序列相关的你今天预测错了明天很可能还会以同样的方向错因为数据里的冲击往往持续多期。考虑多步预测场景如果你用滚动窗口预测未来三期的值第一期预测偏低第二期和第三期受同一个真实值趋势的影响也大概率偏低。这种相关性如果被忽略计算标准误时会低估真实的不确定性导致t值被夸大结论容易出现“假阳性”。极端情况下如果所有误差差都是同一个正值比如模型1稳定地比模型2多偏0.1配对t检验会认为差异极其显著但实际上这样的样本只提供了一份独立信息而不是T份。DM检验通过异方差自相关一致HAC估计来调整这一步具体原理我在下一节展开。1.4 能用在哪些场景DM检验的使用范围相当广主要集中在这几类宏观经济预测GDP、CPI、失业率等宏观变量的多模型对比金融市场波动率预测、收益率预测甚至量化选股模型的回测对比供应链与零售销售预测、需求预测、库存优化中的模型选型能源与气象电力负荷预测、风速预测、气温预报的验证甚至机器学习模型对比也可以用但有一个前提数据必须是按时间顺序组织的且误差序列可以对应到每个时间点。交叉验证给出的误差虽然也能算DM但如果没有明确的时间结构解释起来会有点勉强。2. 原理拆解DM统计量是怎么构造出来的2.1 损失函数的选择决定“预测准”的定义DM检验本身不绑定任何特定损失函数这意味着你有选择空间。最常见的两个选择是平方损失和绝对损失分别对应MSE和MAE的优化目标。但实际项目中损失函数应该和业务链路对齐库存类决策通常更关心误差的绝对值因为缺货和滞销成本对称金融波动率预测则常用QLIKE损失对低波动期的相对误差更敏感。选定损失函数之后d_t 序列就完全确定了。这里我有一条重要建议在做比较之前就要把损失函数定好不要等结果出来后换一个“对结论有利”的损失。换个损失函数得出的结论可能完全相反这种事后挑检验标准的行为哪怕不涉及学术诚信问题在业务决策里也是灾难。2.2 统计量形式与长期方差假设你计算出了损失差序列 d_t共 T 个观测。样本均值记为d̄ (1/T) Σ d_tDM统计量的形式是DM d̄ / sqrt(V_hat / T)其中 V_hat 是损失差序列的长期方差估计量。这里的关键在于对时间序列的样本均值做推断时方差不能简单用样本方差 σ² 除以 T还要把序列自身的相关性考虑进去。长期方差的定义是V γ0 2 Σ_{j1}^{∞} γ_j其中 γ_j 是 d_t 的第 j 阶自协方差。如果损失差序列没有自相关那么后面所有项都消失V 就等于 γ0统计量退化成普通的t检验如果存在自相关V 会被放大标准误变大DM统计量变小结论更保守也更可靠。2.3 Newey-West修正背后的直觉长期方差理论上要加到无穷阶但我们只有T个样本不可能真的收到无穷阶。Newey和West在1987年提出了用截断加权的方式逼近这个无限和V_hat γ0 2 Σ_{k1}^{L} w_k γ_k其中 w_k 是核权重最常用的是Bartlett核w_k 1 - k/(L1)L 是最大滞后阶数。这个权重函数的作用是让高阶自协方差的贡献逐渐衰减到0避免把有限样本里的噪声当作真实的长期相关性。滞后阶数 L 的选择有经验法则一般取 floor(T^(1/3)) 或 floor(T^(1/4))。如果做的是 h 步滚动预测L 至少要取 h-1因为多步预测的误差天然具有MA(h-1)结构。可以这样理解Newey-West修正你每天记录体重如果相邻几天的体重不是独立波动的比如连续聚餐导致连涨三天那么你估计“平均体重”的置信区间会比独立数据时更宽因为样本里的有效独立信息变少了。Newey-West做的就是把这个“数据独立性打折”的程度量化出来。2.4 检验方向与结果判定DM统计量在原假设下渐近服从标准正态分布。实际操作中你可以先计算统计量再查正态分布表得p值。双侧检验判断“两个模型精度是否有显著差异”单侧检验判断“是否模型A显著优于模型B”或反之。举例来说如果 DM 2.13双侧p值约0.033在5%显著性水平下可以拒绝原假设认为两个模型的预测精度存在显著差异。如果 DM 1.30p值约0.19那就没有理由拒绝“精度相同”的原假设。注意p值并不是差异大小的量度p值很小只说明“差异不太可能由随机波动产生”不说明差异有多大。要描述差异大小报告损失差均值和置信区间更合适。3. 用Python实现DM检验附完整代码3.1 数据结构与前置条件开始写代码之前先明确数据结构。你需要三样东西长度必须一致y_true测试期内的真实值序列y_pred1模型1的预测值y_pred2模型2的预测值然后算出两组误差e1 y_pred1 - y_truee2 y_pred2 - y_true。这里有一个重要提醒两套预测必须是在同一段样本外测试期上得到的否则比较没有意义。建议使用滚动原点预测rolling origin而不是一次性切出一个固定验证集。固定验证集的结果对切分点太敏感滚动方式能让评估更稳定。3.2 手写简化版DM检验函数下面是完整的DM检验实现包含Newey-West方差修正。代码里我刻意不依赖太重的统计库只用NumPy和SciPy方便你理解每一步在干什么。import numpy as np from scipy import stats def dm_test(e1, e2, lossMSE, hNone): Diebold-Mariano检验的简化实现 参数 ---- e1, e2 : array_like 两个模型在相同测试期的预测误差 loss : str MSE 表示平方误差损失MAE 表示绝对误差损失 h : int, optional 预测步长。如果提供了Newey-West最大滞后阶数取 h-1 否则按 T^(1/3) 的经验法则确定 返回 ---- dm_stat : float DM统计量 p_value : float 双侧检验p值 e1 np.asarray(e1) e2 np.asarray(e2) if loss MSE: d e1**2 - e2**2 elif loss MAE: d np.abs(e1) - np.abs(e2) else: raise ValueError(loss 参数只支持 MSE 或 MAE) T len(d) d_mean np.mean(d) if h is not None: L max(1, h - 1) else: L max(1, int(np.floor(T**(1/3)))) # 去均值用于计算自协方差 d_centered d - d_mean # 计算0到L阶样本自协方差 gamma np.zeros(L 1) for k in range(L 1): gamma[k] np.sum(d_centered[:T-k] * d_centered[k:]) / T # Newey-West Bartlett核权重 weights 1 - np.arange(1, L 1) / (L 1) long_run_var gamma[0] 2 * np.sum(weights * gamma[1:]) se np.sqrt(long_run_var / T) dm_stat d_mean / se p_value 2 * (1 - stats.norm.cdf(np.abs(dm_stat))) return dm_stat, p_value这段代码的逻辑就是第二节里的公式算损失差估长期方差再算t值。一个小细节自协方差分母用的是 T 而不是 T-k这是Newey-West原论文的做法保证长期方差估计量在大样本下的非负性。如果你在别的实现里看到分母用 T-k也不用吃惊那只是另一种有限样本修正。3.3 一行代码调用statsmodels版如果你不想自己维护统计细节直接用statsmodels更省事。核心思想是对损失差序列 d_t 做一个只有常数项的OLS回归然后用HAC协方差估计修正标准误。由于回归元全是1常数项的估计值就是 d_mean对应的t值就是DM统计量。import numpy as np import statsmodels.api as sm def dm_test_sm(e1, e2, lossMSE, maxlagsNone): if loss MSE: d e1**2 - e2**2 elif loss MAE: d np.abs(e1) - np.abs(e2) else: raise ValueError(loss 参数只支持 MSE 或 MAE) T len(d) if maxlags is None: maxlags max(1, int(np.floor(T**(1/3)))) X np.ones((T, 1)) model sm.OLS(d, X).fit( cov_typeHAC, cov_kwds{maxlags: maxlags, use_corrected: True} ) dm_stat model.tvalues[0] p_value model.pvalues[0] return dm_stat, p_valuestatsmodels的HAC协方差估计同样基于Newey-West方法use_correctedTrue会做小样本自由度修正。对于样本量不大比如小于100的情况这个选项更稳妥。注意statsmodels默认使用t分布计算p值而手写版用的是标准正态分布两者在大样本下几乎一致小样本时t分布更保守一点。3.4 结果一致性验证写完了函数我习惯先构造几个简单场景验证代码没有低级错误。第一个场景两组误差完全相同。此时每个 d_t 都是0DM统计量必然是0p值是1。这个场景能验证函数至少不会崩溃。第二个场景模型1的误差是模型2的3倍。例如模型2的误差是标准正态随机数模型1的误差是模型2的3倍。这时平方损失差序列明显正偏DM统计量应该很大p值趋近于0。第三个场景给损失差序列注入强自相关。你可以手动构建一组 d_t 0.9 * d_{t-1} ε_t 的序列然后看DM统计量是否比直接用独立假设算出的t值小——如果小说明HAC修正起作用了。这三个小实验跑通之后再对真实数据使用心里就有底了。4. 实战案例谁才是更好的销售预测模型4.1 数据与两个候选模型这部分用一个模拟销售数据演示完整流程。数据生成逻辑是趋势项 季节项 随机噪声。我使用固定随机种子保证你复现时结果可对齐。import numpy as np import pandas as pd np.random.seed(2024) T 180 t np.arange(T) y 50 0.08 * t 4 * np.sin(2 * np.pi * t / 12) np.random.normal(0, 1.5, T)两个候选模型选得很简单目的只是演示检验流程模型A12期移动平均。用过去12个月的真实值平均作为下一期预测。模型Bnaive持久化预测。直接用上一期真实值作为下一期预测。在带明显趋势的数据上12期移动平均会明显滞后于真实走势naive模型因为只使用最近一期信息反而能更紧地跟上趋势。于是理论上模型B应该优于模型A。我们做最后60期的滚动一步预测每期只使用当前位置之前的数据模拟真实的预测环境。test_start T - 60 pred_a np.zeros(60) pred_b np.zeros(60) for i, idx in enumerate(range(test_start, T)): if idx 12: pred_a[i] np.mean(y[idx-12:idx]) else: pred_a[i] y[0] pred_b[i] y[idx-1] y_test y[test_start:] e_a pred_a - y_test e_b pred_b - y_test4.2 损失计算与DM检验现在计算两组误差的MSE、MAE并调用我们写的DM函数。在我的环境中一次运行得到的结果如下指标模型A12期MA模型BnaiveMSE7.214.85MAE2.311.89从数值上看模型B明显更优。但这是否能下“B显著优于A”的结论需要看DM检验。以MSE为损失函数做双侧检验dm_stat, p_value dm_test(e_a, e_b, lossMSE) print(fDM {dm_stat:.3f}, p {p_value:.4f})输出大致是DM 2.847, p 0.0044这里d_t e_a² - e_b²均值为正说明模型A的平方误差平均大于模型B。DM统计量2.85p值0.004在1%显著性水平下拒绝“精度相同”的原假设。结论是模型B在平方误差口径下显著优于模型A。4.3 结果解读与实际意义这个结论在业务上怎么用第一可以在汇报里写“基于滚动原点预测模型B的MSE比模型A降低了32.7%DM检验显示差异显著DM2.85p0.004”。第二有条件的话再给出损失差均值及置信区间。损失差均值这里约等于7.21 - 4.85 2.36标准误约2.36/2.85 0.8395%置信区间大致是[0.73, 3.99]。第三也是更重要的单独看这个DM结论还不够最好把MAE口径也测一遍。如果两种损失函数下都显著结论更扎实。我这里只用MSE演示但实际项目中建议至少跑MSE和MAE两个口径并在报告中说明损失函数的选择依据。如果你复现的结果和我这里的数值有些出入不用太担心。numpy版本差异可能导致随机数序列不完全一致但显著性方向应该是稳定的——在这个数据生成过程下naive优于12期移动平均的效果非常明显。5. 避坑指南DM检验的常见误用与替代方案5.1 样本量不足时怎么办DM检验是渐近检验理论性质依赖大样本。经验上测试期少于30个时间点就要非常小心少于50个点时也不能盲目信任正态近似。这时候有两个补救办法使用Harvey、Leybourne和Newbold在1997年提出的小样本修正把DM统计量乘上一个修正因子然后用t分布计算p值。使用bootstrap方法估计p值对损失差序列进行重抽样构造经验分布。用statsmodels做HAC回归时use_correctedTrue其实已经包含了一部分小样本修正但效果不如专门Harvey修正彻底。如果你的样本量确实很小建议手写Harvey修正。5.2 多步预测误差重叠问题做h步滚动预测时每期的预测误差之间存在重叠第t期的预测误差和第t1期的预测误差共享了一部分真实走势信息因此损失差序列天然具有MA(h-1)结构。如果忽略这一点Newey-West的滞后阶数设置得太小标准误会被低估DM统计量虚高。做法是当预测步长为h时把Newey-West的最大滞后阶数至少设成h-1。这是我在实际项目中踩过的坑——刚开始做12步预测时没注意DM显著到令人兴奋后来把滞后阶数拉到11p值直接翻到0.2。永远记住在多步预测场景下DM检验的“独立性折扣”必须做足。5.3 多个模型同时比较的多重检验问题如果你有5个候选模型两两之间都跑DM检验会得到10个p值。就算所有模型精度其实相同也会有约0.4的概率出现至少一个p值小于0.05因为每次比较的假阳性概率叠加。这时候需要用Bonferroni校正或Benjamini-Hochberg的FDR控制方法。更现代的替代方案是模型置信集Model Confidence SetMCS检验它能够同时比较多个模型并输出一个包含最优模型的置信集合。如果项目里频繁面临多模型选型建议认真研究一下MCS它能避免两两比较带来的多重检验麻烦。5.4 嵌套模型与DM检验的效能损失当模型A是模型B的特殊情况时比如AR(1)是AR(2)的嵌套子模型DM检验的功效会明显下降。这是因为嵌套模型之间的误差差异非常小而且高度相关HAC修正会把标准误放大到难以检测的程度。这种情况下推荐使用Clark-West检验2007或Giacomini-White检验2006。Clark-West检验专门针对嵌套模型设计在零假设下统计量更稳定Giacomini-White则更灵活可以处理条件预测能力的比较允许使用滚动窗口评估。遇到嵌套模型比较时不要再死磕DM检验了。5.5 我在实操中的几个习惯最后分享几个自己固化的操作习惯。第一跑DM检验之前一定会先画损失差序列的自相关图。如果自相关很强说明检验结果对滞后阶数敏感我会在报告中同时给出不同滞后阶数下的DM结果让结论更透明。第二凡是做模型比较的汇报必须同时给出损失差异、DM统计量、p值和置信区间四件套缺一不可。第三从不只依赖DM检验做决定——统计显著只回答“差异是不是真的”业务上还要看损失差值的绝对量级够不够覆盖切换成本。一个p值0.0001但MSE只小了0.01的模型不值得你为它重构线上系统。回看这些年做预测项目的经历我最大的体会是模型比较不是“谁的数字小谁就赢”的简单游戏。同样一段数据、同样两个模型换一个测试窗口、换一种损失函数、换一个自相关修正方式结论都可能翻转。DM检验的价值在于强迫你把“比较预测模型”这件事从直觉判断变成可重复、可审计的统计流程。它不能替你拍板用哪个模型但能让你在拍板的时候知道自己到底在冒多大的统计风险。