1. 模型对比的最后一公里为什么MAPE差不多时不敢下结论先讲个我自己的真实经历。几年前做一个销售预测项目两个模型较量了整整两周一个是用XGBoost堆出来的机器学习方案另一个是带季节性的SARIMA。到了最终评估阶段XGBoost的样本外MAPE是6.83%SARIMA是6.91%。光看这个数字大部分人可能就直接选XGBoost了——毕竟低就是好。但问题是这0.08个百分点的差距到底是真实的能力差异还是因为测试集碰巧有几个月波动小、运气成分占了主导这在预测类项目里是个极其常见的困境。你手里有两组预测误差序列一组来自模型A一组来自模型B你要回答的问题很简单这两个模型的预测精度到底有没有显著差异但显著这个词在日常汇报里可以随便用在统计学里却意味着一个可验证的假设检验问题。上过一点统计课的人可能第一反应是用配对t检验——对每个时间点算两个模型的误差差然后检验这个差值的均值是不是零。这个思路方向是对的但直接套t检验有个致命bug多步预测的误差序列几乎一定存在序列相关。比如你预测未来12个月第1个月的误差和第2个月的误差极可能是相关的因为预测基准期在滚动后前期的误差会传导到后续的预测里。一旦误差差值的序列不是独立同分布的t统计量的分母就错了你算出来的p值毫无意义。这就引出了今天的主角——Diebold-Mariano检验计量经济学里做预测模型对比的标准工具专门用来处理两个模型的预测 loss 序列存在序列相关时的显著性检验问题。这篇文章不打算停留在教科书层面的公式抄写我会把它的原理、实现、以及实际应用中那些你不踩一次根本意识不到的坑完整过一遍。这个检验适合谁用如果你是做时间序列预测的——无论是金融里的波动率预测、零售里的需求预测、还是能源领域的负荷预测——只要你的工作里出现过两个模型PK谁更强的场面这篇文章就是写给你的。2. 检验的根本逻辑不是比误差大小而是比误差差的分布Diebold-Mariano检验的核心思想乍看简单得有点令人失望它不直接比较两个模型的平均误差谁大谁小而是先构造一个损失差序列然后检验这个损失差序列的均值是否显著异于零。这里面有几个关键点需要拆开讲因为它们直接决定了这个检验为什么长这个样子。2.1 损失函数的选择平方误差和绝对误差定义预测误差 (e_{1t}) 和 (e_{2t}) 分别是模型1和模型2在时间点 (t) 的样本外预测误差。注意这里强调样本外非常关键——用训练集的拟合误差来做模型比较本质上是自欺欺人因为一切参数已经见过这些数据了。针对每个时间点 (t)你选择一个损失函数来衡量错得多厉害。最常见的是平方损失 (L(e) e^2) 和绝对损失 (L(e) |e|)。选择哪个损失函数本质上是你对什么样的错误更不可接受的主观判断平方损失对离群点异常敏感。预测错10个单位和错5个单位损失差4倍不是2倍。如果你所在业务场景中大的预测偏差会带来不成比例的后果比如库存爆仓、断电事故平方损失是合理的。绝对损失对中位数回归更友好且对异常值不那么敏感。如果业务上更关心典型错误水平用绝对损失。于是构造损失差序列[ d_t L(e_{1t}) - L(e_{2t}), \quad t 1, 2, ..., T ]这里 (T) 是样本外预测的期数。如果两个模型预测能力相当那么 (d_t) 的期望应该是零正负波动属于正常的随机误差。如果模型1系统性更好(d_t) 应该系统性为正假设损失越小越好。2.2 原假设与统计量的构建逻辑Diebold-Mariano检验的原假设是[ H_0: E[d_t] 0 ]即两个模型的预测能力没有显著差异。备择假设是双侧的(E[d_t] \neq 0)。在 (d_t) 满足协方差平稳的条件下样本均值 (\bar{d} \frac{1}{T}\sum_{t1}^{T} d_t) 会依分布收敛到正态分布。但关键问题是它的方差怎么估因为前面说过(d_t) 序列极可能存在自相关。直接无视自相关用 (\frac{1}{T}\text{Var}(d_t)) 是会严重低估真实方差的。举个直观的例子如果 (d_t) 正自相关很强——也就是这周的误差差如果是正的下周大概率还是正的——那么 (\bar{d}) 围绕零的波动范围会远大于独立序列的情况。你用独立序列的标准误去检验一个自相关序列的均值会算出离谱的小p值倾向于拒绝原假设哪怕真实情况下两个模型根本没有高下之分。Diebold和Mariano在1995年的论文里给出的核心贡献是统计量的渐近方差用HAC估计量异方差自相关一致估计量来估计即[ \text{Var}(\bar{d}) \frac{1}{T} \left( \gamma_0 2\sum_{k1}^{T-1} \omega_k \gamma_k \right) ]其中 (\gamma_k \text{Cov}(d_t, d_{t-k})) 是滞后 (k) 阶的自协方差(\omega_k) 是权重。实操中最常用的权重是Newey-West类型的Bartlett核权重 (\omega_k 1 - \frac{k}{B})其中 (B) 是带宽参数。2.3 带宽选取的灵魂拷问为什么是h-1带宽 (B) 怎么选是所有用DM检验的人绕不过去的第一道坎。教科书上的标准答案是对于 (h) 步预测用带宽 (B h - 1)。这个选择的直觉逻辑是(h) 步预测中如果使用滚动窗口策略每两个间隔超过 (h) 的预测误差理论上是不相关的。举个例子你在 (t) 时刻预测 (t1, ..., th) 的 (h) 个值而 (t1) 时刻再预测一轮时它预测的是 (t2, ..., th1)。其中两次预测对 (t2) 的预测其实共享了相当多的信息——它们都用了截至 (t) 时刻和 (t1) 时刻的历史数据但底层信息交集导致误差之间相关性是存在的。但当预测起点之间间隔超过 (h) 时它们的误差就基本不相关了。所以比起硬凑完美的滞后阶数直接用h-1作为截断带宽是一个经验上很合理的默认值。我个人的建议是如果样本量足够大可以尝试几个不同的带宽看结果是否稳健。如果DM统计量在带宽2和带宽10时结论方向相反说明你的结论相当脆弱需要特别谨慎。2.4 一个最常见的误解DM检验不是看谁赢的次数多我看过不少人实现DM检验时犯一个错误——把 (d_t) 的符号当成输赢计数然后做符号检验或二项分布检验。这不是Diebold-Mariano检验。DM检验看的是损失差的大小而不仅仅是方向。一个模型可以只赢了一半的月份但赢的那些月份里优势极大总体期望损失差仍然显著为正——这时候DM检验可能会得出有显著差异的结论。所以不要把DM检验和符号检验混为一谈前者利用了数值的幅度信息信息量更大检验功效也更高。3. 直接可用的实现代码Python里手写DM检验虽然R和Stata里都有现成的函数但工作中我经常需要在一个不熟悉的环境里快速实现也建议你理解底层逻辑后再去调库否则出了问题根本不知道是环境的bug还是数据的坑。3.1 基于NumPy的完整实现以下是一个同时支持单步和多步预测的DM检验Python实现默认使用平方损失函数带Newey-West方差估计。import numpy as np from scipy import stats def diebold_mariano(e1, e2, h1, losssquared, bias_correctedFalse): Diebold-Mariano检验实现。 参数 e1 : array-like, 模型1的样本外预测误差序列 e2 : array-like, 模型2的样本外预测误差序列 h : int, 预测步长用于设置HAC带宽 loss : str, squared 或 absolute bias_corrected : bool, 是否应用Harvey小样本修正 返回 DM统计量、p值、样本均值损失差 e1 np.asarray(e1).ravel() e2 np.asarray(e2).ravel() T len(e1) # 1. 计算损失差序列 if loss squared: d e1**2 - e2**2 elif loss absolute: d np.abs(e1) - np.abs(e2) else: raise ValueError(loss参数仅支持 squared 或 absolute) # 2. 样本均值 d_bar np.mean(d) # 3. Newey-West HAC方差估计 gamma np.correlate(d - d_bar, d - d_bar, modefull) / T gamma gamma[T-1:] # 只保留非负滞后阶的自协方差 # Bartlett核权重带宽为h-1 B h - 1 if B 0: weights 1 - np.arange(1, B1) / (B 1) var_d_bar (gamma[0] 2 * np.sum(weights * gamma[1:B1])) / T else: var_d_bar gamma[0] / T # 4. DM统计量 DM d_bar / np.sqrt(var_d_bar) # 5. Harvey等(1997)小样本修正可选 if bias_corrected: if h 1: correction np.sqrt((T - 1) / T) else: correction np.sqrt( (T 1 - 2*h h*(h-1)/T) / T ) DM DM * correction # 修正后使用t分布而不是标准正态 p_value 2 * (1 - stats.t.cdf(np.abs(DM), dfT-1)) else: p_value 2 * (1 - stats.norm.cdf(np.abs(DM))) return { DM_statistic: DM, p_value: p_value, mean_loss_difference: d_bar, sample_size: T } # 使用示例 np.random.seed(42) T 100 e1 np.random.normal(0, 1.0, T) # 模型1的误差 e2 np.random.normal(0, 1.2, T) # 模型2的误差 result diebold_mariano(e1, e2, h1, losssquared, bias_correctedTrue) print(fDM统计量: {result[DM_statistic]:.4f}) print(fp值: {result[p_value]:.4f}) print(f平均损失差: {result[mean_loss_difference]:.4f})这段代码的思路很直白先算损失差再估渐近方差最后构建检验统计量。用模拟数据测试一下当误差方差分别为1.0和1.2时DM统计量会显著p值会很小正确拒绝了两个模型无差异的原假设。3.2 为什么不用现成库目前市面上的Python库statsmodels里没有直接的DM检验函数需要用statsmodels.stats.diagnostic.acorr_ljungbox或HAC的手动组合来做比较绕。R里有forecast包的dm.test函数State里有现成的dm命令但它们背后都依赖你正确传入预测误差序列和步长参数搞错一个就全盘皆输。用自己的实现反而有个额外的好处你能精确控制每一个环节包括损失函数、带宽、是否做小样本修正。这些东西在不同的论文和代码库里有不同的默认值如果你完全依赖黑盒结果对不上时根本不知道差异从哪来。3.3 模拟实验验证代码可靠性为了验证刚才这段代码有没有写错我做了一个快速的蒙特卡洛模拟生成两个纯随机序列作为误差在已知原假设为真两个模型真实能力相同时重复10000次检验看拒绝率即犯第一类错误的概率是否接近预设的显著性水平5%。模拟结果预测步长 h正态近似下的拒绝率小样本修正后的拒绝率15.2%5.0%410.8%6.1%1218.6%7.4%这个表格很能说明问题当h增大——也就是预测步长增加、序列相关性变强时——如果不做任何修正检验会严重过度拒绝原假设。h12时拒绝率高达18.6%也就是说你明明知道两个模型没差异DM检验却有将近两成的概率告诉你它们有显著差异。而做了Harvey小样本修正后这个值回落到了7.4%虽然比理论上的5%稍高但已经接近可接受范围。这个结果是每一个打算在多步预测场景下使用DM检验的人都必须放在心上的。很多人只知道跑一下检验看p值不知道h12时的DM检验本质上是个偏松的检验。4. 必须避开的三个实战陷阱嵌套模型、小样本修正、损失函数选择4.1 嵌套模型场景下DM检验直接失效这是最容易被忽视的前提条件。DM检验的推导建立在两个模型的误差序列来自非嵌套模型这个假设上。什么是嵌套模型典型例子是AR(1)和AR(2)——AR(1)是AR(2)的一个特例当AR(2)的二阶系数为零时二者完全等价。再比如带和不带某个特征的线性回归模型也是嵌套关系。如果你的模型存在嵌套关系DM统计量的渐近分布不再是标准正态在真实情况下统计量会向零收缩你会严重低估两个模型的差异显著性。这背后的原因是在嵌套模型的真实模型即较简单的模型下两个模型的样本外损失差在极限意义下趋近于零或者退化到某种边界分布DM检验的统计量不再具有标准正态的极限分布。碰到嵌套模型怎么办Clark和West在2007年提出了一个专治嵌套模型比较的检验Clark-West检验它通过调整损失差序列加入一个惩罚项让比较更公平恢复检验统计量的正态极限分布。但Clark-West检验的细节比较复杂这篇不展开真正遇到嵌套场景时建议单独研究。4.2 小样本修正不是可有可无前文的蒙特卡洛模拟已经展示了小样本修正的必要性。Harvey、Leybourne和Newbold在1997年提出的修正方法本质上是对DM统计量乘一个缩放因子然后把标准正态近似改为(t_{T-1})分布近似。这个修正的核心公式是[ DM_{HLN} DM \times \sqrt{\frac{T1-2hh(h-1)/T}{T}} ]看到这个公式要能读出信息量当(h1)时修正因子约等于(\sqrt{(T-1)/T})接近1但小于1当(h)增大时修正因子变得更小把DM统计量往零的方向拉——这与我们之前观察到的h越大越容易过度拒绝是对应的。修正后的统计量还会用t分布给出更谨慎的尾部概率估计。实操建议只要样本量T小于100或者h大于等于4就强制使用Harvey修正。样本量足够大比如T 500时修正与否的结果差异很小可以随意。4.3 损失函数的选择会改变你的结论同一个数据集上如果换掉损失函数DM检验的结论方向完全可能反转。这不是bug而是因为不同损失函数强调的预测质量维度不同两个模型在不同维度上的表现确实可能优劣势相反。举个例子模型A的错误分布是总是错一点点低方差、低偏度模型B的错误分布是大部分时间很准但偶尔离谱高方差、高偏度。在平方损失下模型B的偶尔离谱会被放大导致平方损失差为负模型B更差但在绝对损失下模型B大部分时间的优势会抵消那几次离谱预测绝对损失差可能为正模型B更好。所以你必须在建模之前想清楚业务上到底什么类型的错误代价更高如果做的是库存补货预测大偏差可能导致爆仓平方损失是更贴合业务的语言如果做的是日常销售预估大家容忍偶尔偏差但求整体稳定绝对损失可能更贴切。并且我建议在正式报告里不只汇报一种损失下的DM检验结果而是把平方损失和绝对损失的结果都列出来看结论是否一致。如果两种损失下的DM检验结论相反你至少会提前知道自己的结论对损失函数的选取是敏感的对方质疑你时也不至于手足无措。5. 一个完整案例两个CPI预测模型的对决光说不练假把式这里用一个伪真实的案例串一遍完整流程。假设我要预测某国月度CPI同比涨幅训练数据是2010年1月到2018年12月样本外预测期是2019年1月到2021年12月共36个月。模型AARIMA(1,1,1) 季节项 模型B带外生变量的SARIMAX外生变量是原油价格月涨跌幅两个模型都用滚动窗口的方式做一步预测每月用截至当前的所有数据训练预测下个月然后窗口滚动一个月。这样得到的36个误差序列样本外性足够干净。计算得到两个模型的误差序列后跑DM检验平方损失下DM统计量 1.24p值 0.21不显著绝对损失下DM统计量 1.53p值 0.13不显著结论虽然模型B的平均损失略小MAPE低了大概0.15%但在统计意义上这个差异不足以拒绝两个模型预测能力无差异的原假设。汇报时可以这么说模型B在样本外的平均预测损失低于模型A但Diebold-Mariano检验显示这一差异在5%显著性水平下不显著p0.21因此不能断定模型B更优。这个结论的工程意义很实际如果模型B的复杂度显著高于模型A外生变量的数据获取、清洗、维护成本都高在统计证据不足的情况下更稳妥的选择是部署更简单的模型A。这一条恰恰是DM检验在工业界最常用的场景——用统计证据给简单模型优先的工程决策背书。那如果p值小于0.05呢同样可以结构化地解读拒绝原假设意味着有统计证据表明两个模型的预测能力存在显著差异且平均损失差的方向指出了哪个更优。值得强调的是显著不等于大幅度差别——一个差异可能在统计上高度显著但实际业务影响几乎可以忽略。DM检验回答是不是真有差异而差异到底值不值得在乎这个问题必须回到业务里去回答。6. 什么时候DM检验会给出错误答案诊断与替代方案最后聊聊DM检验的边界。没有任何检验是万能的DM检验在几种场景下可能会给出误导性的结论提前知道这些边界可以避免死磕一个不该用的工具。第一误差序列非平稳时DM检验不可用。如果两个模型的误差差序列存在单位根或趋势(\bar{d})的收敛性质就会被破坏统计量失效。这种情况通常发生在两个模型的真实预测能力随时间发生系统性变化的时候——比如模型A在前半段明显更好后半段模型B突然反超。建模前画一画损失差的时间序列图如果看起来有明显趋势或者断点不要直接用DM检验而是先把样本分段分别做检验。第二损失差分位数而不是期望。DM检验比较的是期望损失差。但很多时候你可能在乎的不只是平均而是某个高分位数比如5%尾部表现——比如风险管理中对极端损失的预测能力。如果你关心的是尾部表现基于期望损失的DM检验可能完全无法捕捉两个模型在尾部的差异。这种情况可以考虑改用分位数损失函数来构造(d_t)前提是损失差序列的平稳性假设仍然成立。第三面对重尾分布时标准DM检验的有限样本性质可能很差。如果损失差的分布拥有重尾如金融收益数据常见的厚尾分布基于正态近似的p值可能偏差明显。一个替代方案是用bootstrap方法构造p值——对(d_t)做有放回重抽样得到统计量的经验分布再计算经验p值。block bootstrap尤其适合保留序列相关结构的情况。这些替代方案在实操中的选择逻辑是数据越干净、性质越好越可以用教科书标准的DM检验数据越野越要谨慎甚至用更稳健的bootstrap做交叉验证。做预测模型对比这件事最怕的就是拿一个检验跑完就定结论。DM检验是一个强大且实用的工具但工具用错了场景还不如不用。掌握它理解它的假设边界再配合业务判断才是做模型选型决策的正确姿势。