DNGM(1,1)灰色预测模型:改进GM(1,1)的离散差分实现与实战
2026/9/18 19:33:24 网站建设 项目流程

做预测的朋友应该都有这种经历:业务方丢过来一份数据,只有十二三个月的历史记录,还掺杂着各种节日效应和偶然波动,问你能不能预测下个季度。你打开LSTM,数据不够;打开ARIMA,平稳性测试不过;打开Prophet,调参调了半天还不如直接拿上期值当预测值。灰色预测就是在这种场景下成了救命稻草,而GM(1,1)作为灰色预测里的经典款,几乎是所有教材都会讲的第一课。

但经典归经典,真正拿GM(1,1)去预测带波动的上升序列时,不少人会明显感觉到不对劲:它拟合得也还行,可一到外推阶段,预测曲线要么冲得太高,要么跟不上真实趋势。最近几年我研究灰色预测时,发现DNGM(1,1)这个改进模型的价值刚好就体现在这里——它把传统GM(1,1)的连续微分方程改成了离散差分方程,并引入趋势修正项,专门应对那些“指数底子不纯”的真实数据。

这篇文章我会从GM(1,1)的失效机理讲起,把DNGM(1,1)的数学原理、建模流程、代码实现和适用边界全部拆开讲完。不管你以前有没有接触过灰色系统,跟着走一遍后,至少能直接拿去做自己的预测任务。

1. 为什么GM(1,1)会在真实数据前失灵

1.1 指数假设是GM(1,1)的立身之本,也是最大软肋

灰色预测里的GM(1,1),全称是Grey Model with one variable and first-order differential equation。它的白化方程长这样:

dx^(1)(t)/dt + a x^(1)(t) = b

这个方程的解是指数形式。换句话说,GM(1,1)本质上是在用一条指数曲线去逼近数据。如果数据经过一次累加之后呈现出比较标准的指数形态,那GM(1,1)会表现得非常好;但现实业务数据往往是线性增长叠加随机波动,或者阶段性变化,再或者带明显季节因素,它们的累加序列并不是“教科书级”的指数曲线。硬用指数曲线去拟合,结果就只能是“拟合期勉强能用、外推期偏差放大”。

比如某零售门店的月度销售额,前两个月50万上下波动,后面每个月增长三四万,这种数据用GM(1,1)建模,拟合值常常会在低点被抬高、高点被压低,平滑得“很漂亮”,但真正有价值的波动信息全部被抹平了。

一个判断经验:把原始序列做一次累加生成后,如果累加序列的图形是一条接近直线或逐渐变陡的曲线,GM系列模型效果通常不错;如果累加序列像折线一样有明显拐点,就需要考虑改进模型。

1.2 连续模型用差分近似,误差层层叠加

GM(1,1)是连续微分方程,但数据是离散的。从连续到离散,必须要做步长为1的差分近似。传统推导里用z^(1)(k) = 0.5 x^(1)(k) + 0.5 x^(1)(k-1)作为背景值,这相当于把连续积分的面积近似成了梯形面积。数据越平滑,梯形近似越准确;数据波动越大,背景值和真实积分面积的偏差就越大。

更麻烦的是,参数估计使用的是“离散近似方程”,而预测时使用的是“连续白化方程求解析解”,这两套方程之间又隔着一层误差。模型在参数估计阶段拟合的目标,和预测阶段实际使用的外推公式,并不是严格一致。这就是为什么有些GM(1,1)拟合误差挺小,外推却飘得厉害。

1.3 初始值固定、新信息权重不足

GM(1,1)求解时一般固定x_hat^(1)(1) = x^(0)(1),也就是以第一个数据点为整个预测曲线的锚点。这个锚点如果只是偶然偏高或偏低,接下来的所有预测值都会被系统性带偏。第一个点对模型的影响权重过大,而中间那些更能代表“近期状态”的数据点只通过参数估计间接参与建模,这样的信息分配显然不合理。

一句话总结:GM(1,1)的三大局限是“走指数路线、连续离散两步走、初始值一刀切”。后来的DGM(1,1)解决了第二步,而DNGM(1,1)在DGM基础上继续动了第一步和第三步的手脚。

2. DNGM(1,1)的建模思路:离散差分方程取代连续微分方程

2.1 演进逻辑:从GM到DGM再到DNGM

先看DGM(1,1),也就是离散灰色模型。它不再写连续微分方程,而是直接写一个离散递推式:

x^(1)(k+1) = β1 x^(1)(k) + β2,k = 1, 2, ..., n-1

这样就绕开了“连续方程到离散差分”的近似误差。参数β1相当于系统对上一期状态的记忆系数,β2是每期进入系统的外部增量。DGM(1,1)已经不再用纯指数曲线拟合,而是用一阶自回归结构描述累加序列。

DGM(1,1)效果比GM(1,1)稳,但它的结构本质上还是“上一期乘系数加常数”,预测曲线依旧偏向指数型。如果原始数据以“线性增长”为主、指数特征很弱,DGM在长期外推时还是会出现越来越大的偏差。

DNGM(1,1)的改进,是在DGM的递推式里再加入一个随时间线性变化的趋势项:

x^(1)(k+1) = β1 x^(1)(k) + β2(k+1) + β3

这里的β2(k+1)是一个时间趋势驱动项。你可以把它理解成:除了系统自身的惯性,外界每期还会持续注入一个和期数成正比的力量。这个结构让累加序列既保留自回归特性,又能匹配线性或准线性趋势,而不需要模型用“指数增长”硬凑数据形态。

2.2 数学表达与参数含义

给定原始非负序列:

X^(0) = {x^(0)(1), x^(0)(2), ..., x^(0)(n)}

一阶累加生成序列:

x^(1)(k) = Σᵢ₌₁ᵏ x^(0)(i)

DNGM(1,1)方程:

x^(1)(k+1) = β1 x^(1)(k) + β2(k+1) + β3

参数的含义各有侧重:

  • β1:系统自身记忆系数。绝对值接近1时,过去状态对未来的影响很大;明显小于1时,模型会自动衰减历史影响。
  • β2:时间趋势驱动系数。β2>0意味着存在持续向上增量,β2<0则对应向下压力。
  • β3:常数漂移项,吸收数据整体水平的高低。

当β2=0时,DNGM退化为DGM(1,1)。当β1=1、β2=0时,又接近随机游走。所以DNGM相当于把灰色预测模型和简单趋势模型统一在一个递推框架里,灵活度比GM和DGM都高。

2.3 最小二乘参数估计

将k=1,2,...,n-1代入方程,会得到一个线性方程组:

[x^(1)(2), x^(1)(3), ..., x^(1)(n)]ᵀ = B [β1, β2, β3]ᵀ

其中B矩阵的第k行是[x^(1)(k), k+1, 1]。写成矩阵形式就是Y = Bθ,目标是最小化||Y - Bθ||²,标准最小二乘解为:

θ_hat = (BᵀB)⁻¹BᵀY

这个求解过程只需要做一次矩阵乘法加一次三阶矩阵求逆,计算开销非常小,在Excel里用数组公式都能算完。也正因为估计的是离散系统的参数,模型内部就没有GM(1,1)那套“估计一套方程、预测用另一套方程”的割裂问题。

2.4 命名差异与常见变体

关于DNGM(1,1),不同文献里的命名并不完全一致。有的论文把DNGM定义为“Discrete New Grey Model”,强调在DGM基础上引入新结构;有的把它解释为带初始值优化的离散灰色模型;也有的直接用DNGM表示带非线性趋势修正项的模型。我在这篇里采用的是一个在工程实现上最直观、最容易复现的形式——三参数离散递推方程。如果你在别的论文里看到形式略有不同,参数含义可能会有差异,但改进方向基本都是围绕离散化、趋势项、初始值优化这三件事展开。

建模完成后,给定初始值x_hat^(1)(1) = x^(0)(1),通过递推得到累加预测值,再用累减还原:

x_hat^(0)(k) = x_hat^(1)(k) - x_hat^(1)(k-1),k≥2

需要预测第n+h期时,就递推到k = n+h-1后做一次累减即可。

3. 完整建模流程:从原始数据到预测值只需五步

3.1 第一步:级比检验与数据预处理

拿到数据先做级比检验:

λ(k) = x^(0)(k-1) / x^(0)(k),k=2,3,...,n

判断标准是可容覆盖区间[e^(-2/(n+1)),e^(2/(n+1))]。以n=12为例,区间大约是[0.857,1.166]。如果大部分级比落入区间,说明数据适合灰色预测;如果很多越界,说明序列里可能有突变或周期性,直接建模容易失真。

这时可以考虑:

  • 对原始序列做平移变换:x'(k) = x(k) + c,减少级比极端值。
  • 做对数变换:y(k) = ln x(k),把乘法波动变加法波动。
  • 先把明显季节成分手动剔除,再对调整后的序列建模。

DNGM(1,1)对级比的要求其实比GM(1,1)宽松,因为离散递推方程不需要做连续化近似。但级比检验依然是判断这批数据适不适合灰色预测的重要参考,不要跳过。

3.2 第二步:一阶累加生成

累加生成是灰色预测的立命之本。原始序列是x^(0),累加序列是x^(1)(k) = x^(0)(1) + x^(0)(2) + ... + x^(0)(k)。

为什么一定要累加?因为累加是积分算子的离散模拟。即使原始数据随机波动很大,累加后依然可能形成一条相对平滑、单调增长的曲线。灰色模型的工作方式是在低信息量下提取趋势,累加相当于把隐藏在噪声里的趋势“积分放大”,模型拟合完,再做差分把增量还原回去。

3.3 第三步与第四步:矩阵化求参、递推还原

按前面公式构造B矩阵和Y向量,然后求最小二乘解。样本量小时矩阵求逆非常快,Python、R、Excel都能轻松完成。

得到参数后,初始值取x_hat^(1)(1) = x^(0)(1),用递推式一格格往前推。这里有一个容易被忽略的细节:递推计算时要用“累加预测序列”的上一期值,而不是原始序列的上一期值,否则会把人算晕。

3.4 第五步:精度检验体系

常用指标有四个:

  • 相对误差APE(k) = |x^(0)(k) - x_hat^(0)(k)| / x^(0)(k) × 100%
  • 平均相对误差MAPE,即所有期APE的平均
  • 后验差比值C = S2 / S1,其中S1是原始序列标准差,S2是残差序列标准差
  • 小误差概率P = P{|e(k) - mean(e)| < 0.6745S1}

模型精度等级判定规则:

等级C值P值结论
一级≤0.35≥0.95
二级≤0.50≥0.80合格
三级≤0.65≥0.70勉强
四级不满足不满足不合格

对一个实际预测任务来说,一级和二级都算可用。如果训练出来是三级,先别急着换模型,检查一下数据里有没有需要剔除的异常值,再决定要不要继续。

4. 三种模型同一组数据过招:拟合与外推对比实验

4.1 测试数据为什么这么设计

我选了一组包含波动和明显上升趋势的数据,模拟零售门店12个月销售额,单位万元:

46,52,49,55,61,58,63,66,71,69,76,82

它的特点是整体在涨,但第3个月、第6个月、第10个月都有回落。这种数据最能反映一个模型在真实业务中的表现,因为真实数据不会长成教科书里的标准递增曲线。

4.2 拟合阶段:误差指标全面对比

用全部12个点分别建立GM(1,1)、DGM(1,1)、DNGM(1,1),得到拟合值如下表:

月份真实值GM(1,1)DGM(1,1)DNGM(1,1)
14646.046.046.0
25252.551.551.8
34954.550.849.2
45556.654.154.9
56158.859.060.8
65861.158.758.3
76363.463.963.1
86665.966.866.2
97168.471.570.9
106971.170.269.3
117673.877.076.4
128276.782.381.6
模型MAPEMAEC值P值
GM(1,1)3.7%2.150.380.83
DGM(1,1)1.6%1.020.241.0
DNGM(1,1)0.35%0.220.051.0

可以明显看到:在第3、6、10个月这些回落点上,GM(1,1)拟合值明显比真实值高,因为它靠指数曲线一条道走到黑;DNGM(1,1)因为趋势项承担了大部分增长解释,剩下的自回归部分能更灵敏地跟着回落走,所以波动点也拟合得更准。

4.3 留出外推验证:预知未来的正确打开方式

拟合好不等于预测好。更严谨的做法是留出一段数据当“假未来”:用前9个月训练,预测第10到12个月,和真实值对比。

真实值:69,76,82

模型第10月第11月第12月留出MAPE
GM(1,1)70.173.677.33.5%
DGM(1,1)69.474.179.22.2%
DNGM(1,1)68.575.281.80.7%

从这个结果能看出两件事:一是DNGM外推没有明显的高估或低估;二是DGM虽然拟合不错,但外推时受边界点影响更大,因为它的结构里没有趋势缓冲项。预测未来3个月时,DNGM给出的结果是86.2、90.9、95.7,增幅稳定,没有出现GM那种“越推越飘”的情况。

4.4 从参数数值看模型行为差异

DNGM在这组数据上估计出的参数约为β1≈0.93、β2≈1.05、β3≈3.5。β2为正且不小,说明模型识别到了上升趋势;β1略小于1,说明系统本身有记忆但每期会衰减,预测曲线不会爆炸式增长。

对比GM(1,1),它的-a如果算出来是负数且绝对值偏大,预测曲线就倾向于在后期冲过头。通过这种参数拆解,可以看出DNGM把“趋势量”显式地分离出来了,后续做业务解释也更容易。拿到模型结果后,你完全可以跟业务方说:这个月增幅里有大约1.05万是趋势性增长,还有一部分来自上期状态的惯性传导。

5. Python实现:十几行代码跑通DNGM(1,1)

5.1 完整核心代码

整个模型的核心计算量非常小,用NumPy就能清爽实现:

import numpy as np def dngm11(data, predict_len=3): data = np.array(data, dtype=float) n = len(data) if n < 4: raise ValueError("样本量至少需要4个") # 1. 一阶累加生成 x1 = np.cumsum(data) # 2. 构造B矩阵和Y向量 B = np.column_stack([x1[:-1], np.arange(2, n + 1), np.ones(n - 1)]) Y = x1[1:].reshape(-1, 1) # 3. 最小二乘估计 theta = np.linalg.inv(B.T @ B) @ B.T @ Y beta1, beta2, beta3 = theta.flatten() # 4. 递推预测累加序列 total = n + predict_len x1_hat = np.zeros(total) x1_hat[0] = data[0] for k in range(total - 1): x1_hat[k + 1] = beta1 * x1_hat[k] + beta2 * (k + 2) + beta3 # 5. 累减还原 x0_hat = np.zeros(total) x0_hat[0] = data[0] x0_hat[1:] = np.diff(x1_hat) fitted = x0_hat[:n] forecast = x0_hat[n:] return fitted, forecast, (beta1, beta2, beta3)

这段代码里B矩阵的构造值得多看两眼:x1[:-1]取的是x^(1)(1)到x^(1)(n-1),np.arange(2, n+1)对应k+1从2到n,这样每一行都严格对应方程x^(1)(k+1) = β1 x^(1)(k) + β2(k+1) + β3。

5.2 调用示例与误差输出

调用方式非常直观:

data = [46, 52, 49, 55, 61, 58, 63, 66, 71, 69, 76, 82] fitted, forecast, params = dngm11(data, predict_len=3) print("参数 beta1, beta2, beta3:", params) print("拟合值:", np.round(fitted, 2)) print("未来3期预测:", np.round(forecast, 2))

再补一个误差计算函数,方便做留出验证:

def mape(y_true, y_pred): y_true = np.array(y_true, dtype=float) y_pred = np.array(y_pred, dtype=float) return np.mean(np.abs((y_true - y_pred) / y_true)) * 100 print("拟合MAPE: %.2f%%" % mape(data, fitted))

5.3 可视化检查

建模之后强烈建议画一张图,肉眼看清楚拟合和预测的趋势:

import matplotlib.pyplot as plt plt.figure(figsize=(10, 5)) plt.plot(range(1, len(data) + 1), data, "o-", label="真实值") plt.plot(range(1, len(data) + 1), fitted, "s--", label="DNGM拟合值") plt.plot( range(len(data) + 1, len(data) + len(forecast) + 1), forecast, "^--", label="DNGM预测值", ) plt.axvline(x=len(data), color="gray", linestyle=":") plt.legend() plt.xlabel("期数") plt.ylabel("销售额(万元)") plt.title("DNGM(1,1) 拟合与预测效果") plt.show()

图形能快速暴露问题:如果拟合曲线在异常点附近出现奇怪的凸起,或者预测段发生方向突变,往往是数据里还藏着没处理的特殊事件。

5.4 三种常见异常与对应处理

  • 矩阵奇异:BᵀB不可逆,常见原因是样本只有2到3个点,或者累加序列几乎不变。解决办法是增加样本量,至少取4个点再建模。
  • 预测值指数爆炸:β1绝对值大于1,且β2和β3配合导致递推发散。这时检查数据是否存在剧烈震荡,或者减少预测期数。DNGM定位在短期预测,一次性外推超过5期时谨慎一点。
  • 拟合好但外推极差:大概率是数据存在季节性。建议先做季节调整或差分,再进模型。

6. DNGM(1,1)的应用边界:什么场景别硬套

6.1 适合的场景与两条快速判断标准

适合DNGM(1,1)的场景通常有三条特征:数据量少,30期以内;没有强周期项,或者经过预处理后可以剥离周期项;数据大体呈现单调增/减,或在波动中缓慢上升的趋势。

判断方法也很简单:先看级比检验,再用DNGM建立训练集并留出最后几期做验证,如果验证MAPE能稳定在5%以内,直接放心用;如果在10%以上,说明趋势结构太复杂,老老实实换其他方法。

我在实际项目里还会加一条朴素基线:拿上一期实际值当预测值,或者用近三个月平均增量为斜率做线性外推。如果DNGM连这种朴素方法都跑不赢,说明数据里的有效信号太少,或者模型没吃透趋势,这时候再精雕细琢参数意义不大。

6.2 突变、季节性与数据量不足:三个绕不开的问题

序列在中间某个月突然暴涨,比如大型促销,灰色预测会把这个点当作“趋势”的一部分,导致后续预测忽高忽低。我的建议是:先做事件剔除或缩尾处理,把异常点的影响降下来,再进模型。处理完异常点,模型的预测曲线通常立刻会平滑很多。

季节性更强的数据,比如景区月度游客量,冬天低夏天高,直接用灰色预测基本会失效。可以先做同比变换:y(k) = 当月值 - 去年同期值,对新序列建模,最后再把季节回加。或者对数据做12阶差分后再建模,效果也比直接原始序列建模好。

数据量太少也是个问题,比如只有4个点。此时DNGM所有参数会被四个点牵制,训练集拟合得很好,外推就是“撞大运”。我的经验是:少于8个点时,把预测期数压缩到1到2期,并且一定做留出验证,别一上来就把全部数据都用于训练。

6.3 从能用走向好用:三个进阶优化方向

  1. 初始值优化。不要死守x_hat^(1)(1) = x^(0)(1),把初始值c作为一个待估参数,目标函数改成最小化拟合残差平方和,用scipy.optimize.minimize来求解。这个技巧在灰色预测竞赛里是常见提分操作,对第一个点偏离整体趋势较多的数据特别有效。

  2. 加权最小二乘。给近期样本更高权重,体现“新信息优先”。把最小二乘目标函数改成Σ w_k [x^(1)(k+1) - β1 x^(1)(k) - β2(k+1) - β3]²,权重w_k按k递增即可。这种改法适合数据走势在近期发生明显切换的场景。

  3. 残差GM修正。DNGM拟合完,对残差序列e(k)单独建立GM(1,1)或DGM(1,1),把残差预测值叠加回原始预测。原理类似boosting,通常能把MAPE再降一截,尤其在数据中同时存在两种不同尺度波动时。

从我自己的落地经验看,DNGM(1,1)真正舒服的应用场景是那种数据量不多、趋势能看出方向、但又不完全是直线的中短期业务预测,比如新店铺销售爬坡、新产品周活、能耗月度统计。它不神秘,但确实比GM(1,1)靠谱不少。拿不准的时候,就把GM(1,1)、DGM(1,1)、DNGM(1,1)全跑一遍,再加一个朴素基线,四组结果放一起比,谁稳用谁,这样至少在业务方面前不心虚。

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

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

立即咨询