ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GM(1,1)灰色预测模型详解:Python实现小样本时间序列预测

GM(1,1)灰色预测模型详解:Python实现小样本时间序列预测 简介灰色预测模型Python实现资料包主要面向数据分析、机器学习初学者以及需要处理非平稳、有噪声数据序列的开发者。资源基于GM(1,1)一阶单变量灰色模型通过多个Python脚本完整演示数据预处理、原始序列生成、一次累加、最小二乘法参数求解、微分方程建立、预测值反演及误差评估等关键环节并附带测试数据文件便于直接运行、对照结果和二次开发。压缩包内共6个文件以.py脚本和.txt数据文件为主整体仅4KB体量精简但流程闭环适合快速理解灰色预测的编程实现。目前已有3907人学习下载可作为课程设计、论文实验或小型工程预测任务的参考实现。学习后可掌握GM(1,1)模型的Python编码思路并能按需扩展至GM(n,1)多阶模型或与ARIMA等方法结合提升预测精度。1. 灰色预测模型不是黑匣子先搞懂 GM(1,1) 在预测什么很多做数据预测的同行一见到“灰色预测”这个词第一反应是“这东西是不是太老了早被深度学习淘汰了”。但真正落到生产环境里尤其是历史数据只有十来个点、业务方又催着要下个月指标的时候GM(1,1) 反而是最不容易翻车的方案。灰色预测模型的本质是对“部分信息已知、部分信息未知”的小样本序列做指数拟合它不需要大量历史数据也不要求数据服从正态分布只要序列满足近似指数增长规律就能用 Python 几十行代码跑出一个可用预测。适合刚入门 Python 数据分析、又不想一上来就碰 LSTM 的读者也适合做销量、客流、能耗等小样本预测时急需一个可解释基线方案的人。它解决的痛点很明确数据少到没法训练神经网络或 ARIMA 时GM(1,1) 用累加生成把随机波动压下去再拟合一阶线性微分方程最后累减还原出预测值。整个过程没有黑匣子每个中间量都能算出来、能检查、能调参。下面我按从建模前检查到代码实现再到避坑的顺序把这个模型的落地路径完整拆开。2. 建模前的数据准备与级比检验为什么你的数据不能直接塞进模型2.1 数据清洗与时间序列排序任何预测模型的第一步都是把数据整理成“干净、有序、等间隔”的时间序列。灰色预测对数据顺序极其敏感如果原始数据是乱的累加生成序列就会完全失真。我一般会先用 pandas 读入数据检查是否有缺失值和重复时间戳然后按时间升序排序。import pandas as pd import numpy as np # 读入数据假设有两列date 和 value df pd.read_csv(sales.csv, parse_dates[date]) df df.dropna(subset[value]) df df.sort_values(date).reset_index(dropTrue) # 提取原始序列必须是等间隔比如逐月、逐周 x0 df[value].astype(float).values print(原始序列长度:, len(x0)) print(原始序列:, x0)这段代码里dropna丢掉空值sort_values保证时间升序。这里有个容易忽略的问题灰色预测要求序列等间隔如果中间缺了某个月要么补插值要么删除该时间段不能直接把两个不相邻的点当成连续序列。我习惯用df.resample(MS).mean()按月重采样并对缺失月份向前填充但要注意填充值会让模型误以为有真实数据所以填充后最好在后续检验里多留意一下级比值。2.2 级比检验公式与可建模区间不是所有序列都适合灰色预测。GM(1,1) 的基本假设是原始序列经过累加后具有近似指数规律而累加序列的指数性又取决于原始序列的级比是否落在允许区间内。级比定义为相邻两个原始数据的比值λ(k) x0(k-1) / x0(k)当原始序列长度 n 固定时级比的允许区间是 ( exp(-2/(n1)), exp(2/(n1)) )。比如 n10 时区间约是 (0.833, 1.200)。如果所有级比都落在这个范围内说明序列适合直接建模否则需要做平移变换或开方变换。# 计算级比 n len(x0) lambda_k x0[:-1] / x0[1:] print(级比值:, lambda_k) # 允许区间 lower np.exp(-2.0 / (n 1)) upper np.exp(2.0 / (n 1)) print(f允许区间: ({lower:.3f}, {upper:.3f})) # 判断是否全部落在区间内 is_ok np.all((lambda_k lower) (lambda_k upper)) print(级比检验通过?, is_ok)这里的核心逻辑是GM(1,1) 最终用最小二乘法解参数 a 和 b如果级比超出区间累加序列的指数特征就不成立参数解会出现很大的方差预测结果要么过度振荡要么直接发散。我在实际项目里见过最典型的失败案例就是拿一组销售额数据不检验直接跑模型结果未来三期预测值变成负数看完级比才发现原始数据里有几个异常尖峰把级比拉出了区间。2.3 数据变换平移与开方让级比落进区间当级比检验不通过时常见做法是给原始序列加一个常数 c让所有数据变大后级比变得更接近 1。因为级比是相邻比值加上常数会缩小相对差异。还有一个办法是开方或取对数但取对数会改变模型还原方式预测后需要指数还原容易引入偏差平移是最稳妥的。# 找到使级比全部落在区间内的最小平移量 c def find_shift(x0, lower, upper): max_lambda np.max(x0[:-1] / x0[1:]) if max_lambda lower and max_lambda upper: return 0.0 # 如果最大级比超过上限需要加常数压小 c 0.0 while True: c 1.0 new_x x0 c ratio new_x[:-1] / new_x[1:] if np.all((ratio lower) (ratio upper)): return c if c 1e6: raise ValueError(找不到合适的平移量请考虑数据变换) shift find_shift(x0, lower, upper) print(建议平移量:, shift) if shift 0: x0 x0 shift这段代码用暴力搜索的方式找平移量对于生产环境来说效率不高但绝对值小、方法直白。更科学的做法是写一个黄金分割搜索不过对于手头只有十几个点的场景循环几百次也无所谓。平移后建模得到的预测值要记得减去 shift 才是真实预测结果。另外需要注意平移量过大时会稀释原始序列的波动预测曲线会趋于平缓所以如果平移量超过原始序列均值的一半就该考虑是否有异常值干扰而不是一味硬平移。3. 用 Python 从零实现 GM(1,1)核心公式与完整代码3.1 GM(1,1) 的建模步骤累加生成、紧邻均值、参数辨识GM(1,1) 的建模过程可以拆成四个步骤。第一步对原始序列 x0 做一次累加生成1-AGO得到 x1第二步用 x1 构造紧邻均值序列 z1z1(k) 0.5 * x1(k) 0.5 * x1(k-1)第三步根据白化微分方程 dx1/dt a * x1 b用最小二乘法估计发展系数 a 和灰作用量 b第四步把 a、b 代回时间响应式算出预测的累加值再累减还原。这里的紧邻均值序列是很多教程里一带而过但实际容易算错的地方。z1 不是直接用 x1 本身而是相邻两个 x1 的平均值目的是平滑累加序列的跳跃。参数辨识的矩阵形式是u (a, b)^T (B^T B)^{-1} B^T Y其中 B 的第一列是 -z1第二列是全 1Y 是原始序列从第二个点开始的值构成的列向量。3.2 完整 Python 函数输入原始序列输出预测值我封装了一个 GM11 函数输入原始序列 x0 和预测步数 m输出预测值、参数和中间过程。为了可读性我把累加、构矩阵、解参数分开写。def gm11(x0, m5): 灰色预测 GM(1,1) 模型 :param x0: 原始序列一维 numpy 数组或列表 :param m: 预测未来的步数 :return: (predict_values, a, b, x1) x0 np.asarray(x0, dtypefloat) if len(x0) 4: raise ValueError(灰色预测至少需要 4 个样本点) # 1. 累加生成序列 x1 x1 np.cumsum(x0) n len(x0) # 2. 紧邻均值序列 z1 z1 np.zeros(n - 1) for k in range(1, n): z1[k - 1] 0.5 * x1[k] 0.5 * x1[k - 1] # 3. 构造 B 矩阵和 Y 向量 B np.column_stack((-z1, np.ones(n - 1))) Y x0[1:].reshape(-1, 1) # 4. 最小二乘解参数 u (a, b)^T u np.linalg.inv(B.T B) B.T Y a u[0][0] b u[1][0] # 5. 时间响应式x1_hat(k1) (x0[0] - b/a) * exp(-a*k) b/a x1_hat np.zeros(n m) x1_hat[0] x0[0] for k in range(1, n m): x1_hat[k] (x0[0] - b / a) * np.exp(-a * (k - 1)) b / a # 6. 累减还原得到预测值 x0_hat np.zeros(n m) x0_hat[0] x1_hat[0] for k in range(1, n m): x0_hat[k] x1_hat[k] - x1_hat[k - 1] predict x0_hat[:n] future x0_hat[n:] return predict, future, a, b, x1逻辑说明np.cumsum完成累加生成B矩阵的构造完全对应最小二乘公式B.T B可能接近奇异当数据量太小时建议用np.linalg.pinv求伪逆时间响应式里的exp(-a*(k-1))的起始点从 0 开始对应原始第一个值这里容易下标错位我第一次写的时候因为 k 从 0 起导致拟合值整体右移一位后来对照 x0 才发现问题。参数 a 通常是负数表示序列增长趋势a 的绝对值大于 2 时模型不可用这个在避坑章节会细说。3.3 预测结果的后处理累减还原与残差检验上面的函数返回了拟合值predict、未来值future和参数。实际业务里我们最关心的是未来值但必须先把拟合效果验证一下。残差检验是最直接的计算原始值与拟合值的绝对误差和相对误差相对误差通常控制在 5% 以内才能交付。# 使用上面的 gm11 函数 pred, fut, a, b, x1 gm11(x0, m3) # 残差检验 residual x0 - pred relative_error np.abs(residual) / x0 print(平均相对误差: {:.4f}.format(np.mean(relative_error))) print(未来预测值:, fut) # 画图对比 import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # Windows 显示中文 plt.figure(figsize(8, 4)) plt.plot(range(1, len(x0) 1), x0, o-, label原始数据) plt.plot(range(1, len(x0) 1), pred, s--, label拟合值) future_idx range(len(x0) 1, len(x0) len(fut) 1) plt.plot(future_idx, fut, ^--, label未来预测) plt.legend() plt.title(GM(1,1) 预测结果) plt.show()后处理的核心是验证误差是否在可接受范围。如果平均相对误差大于 5%我会优先检查原始数据里有没有异常点或者尝试对原始序列做开方变换。这里有个关键点相对误差是按原始数据大小计算的如果原始序列数值很小比如几百误差 20 可能就占比很高这时要用绝对误差或者 MAPE 的综合视角来判断不能让一个极小的点拉高整体误差率。4. 精度检验与参数调优别只看拟合图要看后验差比值4.1 残差检验、关联度检验、后验差检验怎么算很多人做灰色预测画完拟合图觉得“挺像”就直接交差这是最容易踩坑的。灰色预测有一套标准的精度检验体系残差检验是最基础的关联度检验衡量拟合曲线与原始曲线的几何形状相似性而后验差检验则从统计角度评估预测值的离散程度。后验差比值的计算公式是 C S2 / S1其中 S1 是原始序列的方差S2 是残差序列的方差。C 越小越好说明残差波动相对于原始数据波动很小。通常 C 0.35 为优C 0.5 为合格C 0.65 为不合格。还有一个小概率误差 P P( |e(k) - mean_e| 0.6745 * S1 )P 越大越好一般要大于 0.95。# 计算后验差比值 C 和小误差概率 P def posterior_test(x0, pred): n len(x0) residual x0 - pred S1 np.std(x0, ddof1) S2 np.std(residual, ddof1) C S2 / S1 mean_resid np.mean(residual) threshold 0.6745 * S1 p np.sum(np.abs(residual - mean_resid) threshold) / n return C, p C, p posterior_test(x0, pred) print(f后验差比值 C {C:.4f}, 小误差概率 P {p:.4f})这个检验函数贴到自己的工具库里非常实用。注意这里用的是ddof1即样本标准差因为数据量小用总体标准差会低估方差导致 C 偏小给人一种“精度很高”的假象。我见过有文章教程直接np.std(x0)在小样本下这个差异很可观n10 时偏小约 5%会让不合格的模型被误判为合格。4.2 参数 a、b 对预测趋势的影响以及滑动窗口的选取GM(1,1) 里面 a 和 b 不是摆设。a 代表发展系数它的符号决定趋势方向a 0 表示递增a 0 表示递减|a| 越大指数趋势越陡峭。当 a 的绝对值大于 2 时模型的解会趋于不稳定预测值可能出现指数爆炸这也是灰色预测的一个硬边界。b 是灰作用量它相当于微分方程里的驱动项b 和 a 共同决定预测值的水平。实际使用中我不建议把所有历史数据都塞进模型。灰色预测更适合“近期数据反映当前趋势”的场景。比如你有 24 个月的销量直接拿 24 个点建模旧数据会把近期的增长趋势稀释掉。常见做法是选取最近 5 到 8 个点建模步长太短则随机噪声影响大步长太长则趋势变化被平均。我一般用滚动窗口的方式验证拿前 k 个点建模预测第 k1 个点然后滑动窗口比较多个预测误差选出误差最小的窗口长度。def select_window(x0, max_window10): 滑动窗口实验选择平均相对误差最小的窗口长度 best_win None best_err float(inf) for win in range(4, min(max_window, len(x0) - 1) 1): errs [] for i in range(win, len(x0)): train x0[i - win:i] pred, _, _, _, _ gm11(train, m1) actual x0[i] errs.append(abs(pred[-1] - actual) / actual) avg_err np.mean(errs) if avg_err best_err: best_err avg_err best_win win return best_win, best_err win, err select_window(x0) print(f最优窗口长度: {win}, 平均误差: {err:.4f})这里的逻辑是模拟“用过去 win 个点预测下一点”跑遍整个时间序列取平均误差。注意避免窗口长度等于总长度时没有验证样本所以循环里range(win, len(x0))保证每次预测都有真实值对照。滑动窗口的结果不能只看最低平均误差还要看误差序列是否稳定有些窗口平均误差低但个别点误差超过 30%这种窗口不具备鲁棒性。4.3 等维递补预测滚动更新数据避免预测“发散”灰色预测单次预测多步时随着预测步数增加误差会指数级放大。这是因为每多预测一步就把上一步的预测值当作真实值继续迭代。实际业务里我很少直接一次性预测 12 个月而是采用等维递补滚动预测每次只用最近 n 个真实值建模预测下 1 个值等这个值真实验证后把它加入序列同时丢弃最旧的数据再预测下一个。def rolling_predict(x0, total_forecast6): 等维递补预测每步只预测一个点并滚动更新序列 history x0.copy() results [] for _ in range(total_forecast): # 一般取最近 6 个点建模 train history[-6:] _, fut, _, _, _ gm11(train, m1) next_val fut[0] results.append(next_val) # 删除最旧的数据加入新预测值递补 history np.append(history[1:], next_val) return np.array(results) future_vals rolling_predict(x0) print(等维递补预测结果:, future_vals)这段代码的关键是history[1:]把最早的点丢掉保持序列长度不变。等维递补适合实时预测系统每次新数据到货就更新模型避免预测值发散。代价是如果预测值误差较大它会被当作“真实值”滚入历史导致后续预测被带偏所以我在生产环境里会设置一个异常检测如果某步预测值相对最近真实点变化超过 30%就返回人工复核而不是自动递补。5. 避坑指南灰色预测最容易翻车的 4 个场景5.1 数据里存在负数或零值级比直接爆表现象计算级比时出现负数或无穷大程序直接报错或级比检验永远不通过。原因GM(1,1) 的累加生成和指数响应式都要求原始数据为正。零值导致相邻比值分母为 0负值会让累加序列出现非单调级比完全失真。解决先做平移变换把所有数据抬升到正数区间。我常用min(0, min(x0)) 1做整体平移但要注意平移量不能太大否则会掩盖序列的真实相对变化。更稳妥的做法是换成灰色 Verhulst 模型处理含负值的饱和型序列不过那是另一个话题了。5.2 一次性预测多步结果后期指数爆炸现象用模型一次预测 12 个月前 3 个月看起来挺准第 6 个月开始预测值急剧增大后面直接变成天文数字。原因GM(1,1) 是指数外推当 a 为负且绝对值偏大时累加序列的解本身就是指数增长的多步外推时每一步都基于前一步的预测值误差也被指数放大。解决控制单次预测步数不超过建模样本数的 1/5。比如用 10 个点建模最多预测 2 步。需要长周期预测时改成前面说的等维递补每步只预测 1 个点用真实值或人工核对后的值滚动更新。不要迷信模型能预测得又长又准。5.3 级比检验通过后仍然出现负预测值现象级比检验明明过了模型参数 a、b 也正常但某几个预测点变成负值。原因这个一般发生在原始序列有明显的下降段累加序列出现拐点最小二乘解出的 a 为正但 b 的补偿不足导致时间响应式在某些 k 上越过零点。解决先检查原始序列是不是非单调的。灰色预测最适合单调趋势序列如果序列有周期性或V型反弹要先把周期成分提取掉对残差建模。一个最简单的处理是只对最近一段单调区间建模放弃早期的震荡数据。另一个办法是给预测结果再加一个下限约束不过这只是事后补救模型本身不适用于非单调数据时换模型才是正路。5.4 把灰色预测当黑匣子忽略与业务知识的交叉验证现象预测结果算出来数学上一切合格但业务方直接否掉“这个增长速率不现实。”原因灰色预测只从数据本身提取趋势它不知道业务上限、政策限制或市场容量。比如疫情后客流恢复期模型可能预测下月客流翻倍但实际场地容量有限根本不可能翻倍。解决在模型输出后加一层规则引擎对预测值做业务边界裁剪。我习惯把灰色预测的结果作为“基准预测”再用业务约束比如同比上限、环比上限、容量阈值做区间限制。另外每一次预测都要留下参数 a、b 和级比区间作为模型解释项这样业务方质疑时你能明确说出模型是基于哪段数据、按什么趋势外推的。6. 把 GM(1,1) 封装成可复用类并加一个简单的置信区间最后一章不写总结讲一个我每天在用的进阶技巧把灰色预测封装成 Python 类顺手加上基于残差的置信区间。这样你不需要每次建模都复制那六步代码而且还能复用窗口选择、级比检验、后验差检验这些逻辑。class GreyModel: def __init__(self, x0, windowNone): self.x0 np.asarray(x0, dtypefloat) self.window window if window else len(self.x0) self.shift 0.0 self.a None self.b None self.pred None self.future None def fit(self): # 检查级比必要时平移 n len(self.x0) lower np.exp(-2.0 / (n 1)) upper np.exp(2.0 / (n 1)) ratio self.x0[:-1] / self.x0[1:] if not np.all((ratio lower) (ratio upper)): self.shift np.abs(self.x0.min()) 1 x self.x0 self.shift else: x self.x0 # 此处调用前面定义的 gm11 逻辑 pred, fut, a, b, _ gm11(x, m3) if self.shift 0: pred pred - self.shift fut fut - self.shift self.a, self.b, self.pred, self.future a, b, pred, fut return self def confidence_interval(self, alpha0.95): resid self.x0 - self.pred std np.std(resid, ddof1) # 用 t 分布近似样本少时更稳 from scipy import stats t_val stats.t.ppf((1 alpha) / 2, dflen(resid) - 1) margin t_val * std * np.sqrt(1 1 / len(resid)) return self.future - margin, self.future margin这个类的重点是confidence_interval方法它把残差的标准差转化为预测值的上下浮动带实际业务里我通常会画成阴影区间而不是只给一条曲线。注意这里用的是std和 t 分布近似不是标准正态分布因为小样本下 t 分布的尾部更厚区间更诚实不会给出过度自信的窄带。如果你不想引入 scipy可以用查表法把 t 值近似为 1.96但样本小于 10 时还是建议查 t 表。最后说一个我的习惯每当数据序列更新我不重新全量建模而是保留上一次的窗口长度和参数只把新数据追加进来重新做级比检验和参数估计。如果发现 a 的变化超过 20%我会主动告警因为这说明趋势发生了结构性变化灰色预测的前设已经不稳。这个习惯帮我挡掉了好几次“看着拟合很好、实际预测完全跑偏”的生产事故。希望这些从检验到避坑再到封装的细节能让你在下次接到小样本预测需求时少一点玄学多一点可复现的依据希望帮到你。本文还有配套的精品资源点击获取
返回列表