ARTICLE DETAIL

资讯详情

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

灰色马尔可夫链模型GMCM在人口预测中的原理与MATLAB实现

灰色马尔可夫链模型GMCM在人口预测中的原理与MATLAB实现 简介这是一份基于灰色马尔科夫链模型GMCM的MATLAB人口预测项目实例融合灰色系统理论与马尔科夫链方法面向具备一定编程基础的科研人员、数据分析师及城市规划从业者用于解决人口数据波动性大、样本有限场景下的预测精度问题。资源为docx文档共1个文件压缩包约74KB虽体量精简但内容覆盖模型原理、架构设计与完整实现流程。已有870人学习下载适合需要快速掌握GMCM建模思路并落地代码的读者。文档从项目背景、目标与挑战出发详细拆解了数据预处理、GM(1,1)模型构建、残差分析、状态划分、马尔科夫链转移概率计算、预测修正与结果还原等关键步骤并配有完整代码示例、GUI设计及性能评估方法。读者可直接参照实现也可借鉴其模块化结构进行二次开发应用于城市规划、资源配置、社保与卫生管理等领域。 做人口预测我最早用的就是灰色GM(1,1)那套。刚跑通的时候觉得特别神奇给一组历史人口数据拟合阶段画出来就是一条光滑指数曲线误差很小。但一拿去外推问题就暴露了——真实人口数据根本不是平滑增长的中间总有起伏而这种起伏不是随机噪声而是“有记忆”的偏离。GM(1,1)不管这些它默认残差是白噪声结果预测曲线总是在真实走势的上方或下方系统性漂移。后来在项目里接触到灰色马尔可夫链模型Grey Markov Chain ModelGMCM思路一下子清晰了GM(1,1)负责把握趋势主框架马尔可夫链负责修正残差的状态转移。也就是说把预测值与真实值之间的偏差按大小划分成若干个状态再用状态转移概率去预测未来的偏差谁先谁后出现把这些偏差修正回GM预测值上。这种两层结构的组合用来做人这种“长期有趋势、短期有波动”的人口预测效果比纯灰色模型直观地好上一截。这次我把整个从原理到MATLAB实现、再到GUI封装的过程整理出来里面包含可直接复现的完整程序代码以及我自己调参时踩过的几个坑。适合正在做数学建模、区域发展规划或者毕设里涉及人口预测的读者哪怕你之前完全没碰过马尔可夫链跟着文章把代码跑起来也不难。1. GMCM模型的核心逻辑灰色预测管趋势马尔可夫管残差1.1 GM(1,1)为什么适合做人口趋势建模灰色模型用一阶单变量微分方程描述数据演变规律尤其适合“数据量少、趋势明显、没有完整统计规律”的序列。人口数据正好是这样历史上几十年的人口统计往往只有十几个有效样本点想用ARIMA这种统计模型很容易因为样本不足而失效而GM(1,1)在样本量只有10~15个时表现反而很稳定。它的基本思路很简单原始序列记为[x^{(0)}(k) {x^{(0)}(1), x^{(0)}(2), ..., x^{(0)}(n)}]数据经过一次累加生成1-AGO后变成单调递增序列 (x^{(1)}(k))累加能有效压制原始序列的随机波动。然后对累加序列建立白化微分方程[ \frac{dx^{(1)}}{dt} a x^{(1)} b ]参数 (a) 叫发展系数控制增长强度(b) 叫灰作用量反映数据变化的内在驱动。用最小二乘对紧邻均值生成的背景值序列求解得到时间响应式[ \hat{x}^{(1)}(k1) \left(x^{(0)}(1) - \frac{b}{a}\right) e^{-ak} \frac{b}{a} ]最后通过累减还原得到原始序列的预测值 (\hat{x}^{(0)}(k))。这套模型对指数型增长趋势的拟合能力非常强但它的“光滑性假设”也是最大的缺陷。我在实际项目里多次观察到如果原始数据在某个年份因为政策调整或者统计口径变化出现明显波动GM(1,1)给出的残差并不是随机散布而是一串同正或同负的连续值。这种残差里藏着信息被称为“灰色残差修正的机会”。1.2 马尔可夫链到底修正了什么马尔可夫链是一种无后效性的随机过程模型下一时刻的状态只与当前状态有关与更早的历史无关。放在人口预测场景里意思是——如果今年GM预测比真实值偏低了0.8%那么明年预测偏差继续偏低的概率有多大偏差状态又是怎么转移的具体操作分三步计算历史残差序列 [ \varepsilon(k) x^{(0)}(k) - \hat{x}^{(0)}(k) ]把残差取值范围划分为 (m) 个状态区间比如 ( \varepsilon \in [-1.2, -0.6)) 为状态1([-0.6, 0)) 为状态2([0, 0.6)) 为状态3([0.6, 1.2]) 为状态4。统计状态转移概率矩阵 [ P_{ij} \frac{n_{ij}}{n_i} ] 其中 (n_i) 是历史上处于状态 (i) 的次数(n_{ij}) 是从状态 (i) 转移到状态 (j) 的次数。未来的残差状态就根据当前残差状态按转移概率矩阵推算落到哪个状态就用该状态区间中值或概率加权期望作为残差修正值加到GM(1,1)预测结果上。所以GMCM修正的不是趋势而是“系统性的预测偏差”。如果残差是纯随机的状态转移矩阵会接近均匀分布修正效果不明显如果残差明显聚集在某几个状态并反复转移马尔可夫链就能抓住这个周期把预测精度拉高。1.3 GMCM的整体运算流程我的MATLAB实现里整个流程固定为下面几步输入历史人口数据做级比检验确认数据适不适合灰色建模。调用GM(1,1)主程序得到历史拟合值和未来预测值。计算历史残差按残差大小划分状态区间。统计状态转移概率矩阵。从当前残差状态出发预测未来各步的残差状态。残差状态对应的区间中值加到GM预测值上输出最终GMCM预测结果。这个流程的好处是模块化清晰每个环节都能单独测试。我调模型时习惯先把第2步GM预测跑出来画在图上再叠加第5步的残差修正每一步的效果肉眼可见比直接甩一个大黑盒要靠谱得多。2. 核心代码实现从灰色模型到马尔可夫修正2.1 项目文件结构与示例数据为了阅读和复用方便我把程序拆成四个文件gm11_predict.mGM(1,1)主函数负责拟合与预测。markov_state.m残差状态划分构造状态转移矩阵。gmcm_predict.mGMCM组合入口把前两个模块串起来。gmcm_gui.mlappApp Designer版GUI界面。演示数据我构造了一组S市2011—2022年的常住人口数据单位万人数值上是“缓慢上升、中间带小波动”的形态符合多数城市人口变化特征理解算法时直接替换成统计年鉴数据即可S [142.6, 145.1, 147.8, 150.4, 153.2, 155.0, ... 158.4, 161.2, 163.5, 165.8, 168.8, 170.5];2.2 GM(1,1)主函数的实现下面这个函数是我在项目中一直沿用的版本注释里把矩阵构造和累减还原写清楚了直接用没问题function [xhat_hist, xhat_future, a, b] gm11_predict(x0, steps) % 灰色GM(1,1)预测 % x0 : 历史观测值行或列向量均可 % steps : 未来预测步数 n length(x0); x0 x0(:); % 1-AGO累加生成 x1 cumsum(x0); % 紧邻均值生成背景值 z1 0.5 * (x1(1:end-1) x1(2:end)); % 构造B矩阵和Y向量 B [-z1, ones(n-1, 1)]; Y x0(2:end); % 最小二乘求解发展系数a和灰作用量b u (B * B) \ (B * Y); a u(1); b u(2); % 时间响应式构造从0开始的完整拟合序列 % 多算steps个点保证后面能切出未来预测 T 0 : n steps - 1; x1_model (x0(1) - b / a) * exp(-a * T) b / a; % 累减还原成原始量级 x0_model [x1_model(1), diff(x1_model)]; % 切分历史拟合段和未来预测段 xhat_hist x0_model(1:n); xhat_future x0_model(n1:end); end这里有两个容易写错的地方我单独强调一下。第一T必须从0开始而不是从1开始因为累减还原时第一个点要直接用 (x^{(0)}(1))如果T从1开始模型起点会整体偏移影响后面的残差计算。第二参数求解用的是(B * B) \ (B * Y)而不是直接inv(B*B)*B*Y虽然结果一样但反斜杠运算符在数值稳定性上更好尤其在矩阵条件数较大的时候。2.3 残差状态划分与状态转移矩阵残差状态划分是整个GMCM能不能work的关键。状态分得太少修正精度不够分得太多样本量撑不起转移矩阵会出现大量零概率行。我的经验是历史数据量在10~15个样本时状态数取3~5个最稳。下面是状态划分和转移矩阵构造的完整函数function [states, P, midVec] markov_state(resid, nStates) % 将残差序列划分为nStates个状态 % states : 每个残差所属状态编号 % P : 状态转移概率矩阵 % midVec : 每个状态区间的代表值区间中值 N length(resid); % 等宽划分状态区间首尾边界扩展到无穷 edges linspace(min(resid), max(resid), nStates 1); edges(1) -Inf; edges(end) Inf; % 区间中值处理无穷边界导致的中值不存在问题 midVec 0.5 * (edges(1:end-1) edges(2:end)); midVec(1) edges(2) - abs(edges(2)) * 0.1 - 0.01; midVec(end) edges(end-1) abs(edges(end-1)) * 0.1 0.01; % 左开右闭划分状态 states zeros(1, N); for k 1:N for s 1:nStates if resid(k) edges(s) resid(k) edges(s1) states(k) s; break; end end end % 构造状态转移概率矩阵 P zeros(nStates, nStates); for i 1:nStates idx find(states(1:end-1) i); if isempty(idx) % 该状态在历史中没有出现过为防止NaN做自转移处理 P(i, i) 1; continue; end nextStates states(idx 1); for j 1:nStates P(i, j) sum(nextStates j) / numel(idx); end end end为什么要“左开右闭”因为如果残差值正好落在两个区间的公共边界上而两边都用“闭区间”接收这个样本就会同时归入两个状态状态统计就会出错。左开右闭保证每个样本有且仅有一个归属。edges(1)-Inf和edges(end)Inf的取值是为了防止残差最小值或最大值超出线性划分区间。而中值向量midVec在无穷边界处不能直接求平均我用了向外扩展一小段距离的方式构造代表值这样修正残差时不会出现Inf污染预测值。2.4 GMCM组合入口与修正预测最后把两个模块组合起来生成GMCM修正后的预测结果function [xhat_GM, xhat_GMCM, P, states] gmcm_predict(x0, steps, nStates) % GMCM灰色马尔可夫链人口预测主入口 % 输出GM原始预测、GMCM修正预测、状态转移矩阵、历史状态序列 % 第一步GM(1,1)预测 [xhat_hist, xhat_GM, a, b] gm11_predict(x0, steps); % 第二步计算历史残差 resid x0(:) - xhat_hist; % 第三步残差状态划分与状态转移矩阵 [states, P, midVec] markov_state(resid, nStates); % 第四步从当前状态出发预测未来残差状态 curState states(end); futureResid zeros(1, steps); for k 1:steps % 按转移概率最大值选取下一状态 [~, curState] max(P(curState, :)); futureResid(k) midVec(curState); end % 第五步修正GM预测值 xhat_GMCM xhat_GM futureResid; end这个组合入口里预测未来残差状态时用的是“每一步都取转移概率最大的状态”属于最大概率法。它简单直观适合步数较短的外推。如果你希望结果更平滑也可以改成按状态概率分布随机抽样取期望值效果差别在短期预测内不大。按照这个代码跑一遍拿到的xhat_GM就是纯灰色模型的预测xhat_GMCM是修正后的预测。两者放在一起对比修正效果一目了然。3. GUI设计把模型封装成交互工具3.1 页面布局与控件选择MATLAB建模环境里纯脚本已经能满足功能需求但真要给别人用尤其是写论文、做系统演示或者给导师交差GUI是必须的。我用的是MATLAB App Designer而不是老的GUIDE因为App Designer在控件布局、回调管理和代码维护上更干净R2016a之后的版本都推荐这种方式。界面布局我设计成上下两个功能区域左侧输入区一个大的“历史数据”输入框Edit Field直接用文本形式输入数据一个“预测步数”数字输入框一个“状态数”下拉列表可选3、4、5一个“开始预测”按钮。右侧展示区上方一个坐标轴UIAxes显示历史数据和两条预测曲线下方一个表格UITable显示未来各年的GM预测和GMCM预测数值左下角一个文本框输出模型参数 (a)、(b) 和转移矩阵。整体设计逻辑是“一个按钮完成所有事”不给用户看任何中间变量。运行后界面效果大致就是左边填数据右边立刻出曲线和表格。3.2 回调函数的组织方式App Designer里每个按钮对应一个回调函数。核心按钮“开始预测”的回调代码如下% 按钮点击事件回调 function StartButtonPushed(app, event) % 读取输入数据 dataStr app.HistoryDataEditField.Value; dataArr str2num(dataStr); % 支持直接输入 [142.6 145.1 ...] 或 142.6, 145.1 steps app.StepsEditField.Value; nStates app.StatesSpinner.Value; % 调用GMCM核心预测程序 [gmPred, gmcmPred, P, states] gmcm_predict(dataArr, steps, nStates); % 画图 cla(app.UIAxes); n length(dataArr); histX 1 : n; futureX n1 : nsteps; plot(app.UIAxes, histX, dataArr, o-, LineWidth, 1.2); hold(app.UIAxes, on); plot(app.UIAxes, futureX, gmPred, --*, LineWidth, 1.2); plot(app.UIAxes, futureX, gmcmPred, -s, LineWidth, 1.2); hold(app.UIAxes, off); xlabel(app.UIAxes, 年份序号); ylabel(app.UIAxes, 人口数万人); legend(app.UIAxes, {历史人口, GM(1,1)预测, GMCM修正预测}, Location, northwest); grid(app.UIAxes, on); % 输出预测表格 T table((1:steps), gmPred(:), gmcmPred(:)); T.Properties.VariableNames {预测步数, GM预测, GMCM预测}; app.ResultTable.Data T; % 输出模型参数 app.ParamTextArea.Value sprintf(a %.4f, b %.4f\n状态转移矩阵\n%0.3f, ... a, b, P); % 实际ab需要从gmcm_predict里返回 end需要说明的是上面代码里我为了可读性省略了gmcm_predict返回a和b的部分实际应用时直接在gmcm_predict函数里增加两个输出即可或者把gm11_predict的a、b通过app.UserData传出来。回调函数里最容易翻车的点就是忘记处理输入异常——比如用户输入了非数字字符、数据长度小于状态数、步数为0这些都要在回调开头加判断if length(dataArr) nStates uialert(app.UIFigure, 历史数据量必须大于状态数, 参数错误); return; end3.3 预测结果导出与中间状态存档表格展示只能看当前结果实际项目里导师或甲方通常要求交付Excel文件。这时候可以用MATLAB的writetable搭配uiputfilefunction ExportButtonPushed(app, event) if isempty(app.ResultTable.Data) uialert(app.UIFigure, 请先执行预测, 无结果); return; end [filename, path] uiputfile(*.xlsx, 导出预测结果); if filename 0 return; end writetable(app.ResultTable.Data, fullfile(path, filename)); end还有一类需求是保留中间状态比如想把马尔可夫状态转移矩阵、历史各年残差状态导出来写论文用。我的处理方式是每次预测结束后把中间数据存到app.UserData里需要导出时直接从这里面取。App Designer里每个控件都能访问app.UserData这个万能保存位置比定义一堆全局变量干净多了。4. 用实际数据验证GMCM的效果4.1 样本内拟合与样本外预测对比为了验证模型我以2011—2022年共12个数据点为整体先做一个“向后验证”用2011—2021年的11个数据训练模型预测2022年然后拿预测值和真实值比较。实际跑出来的结果大致是GM(1,1)纯模型预测2022年为169.2万人真实值170.5万人误差1.3万人相对误差0.76%。GMCM修正后预测2022年为170.7万人相对误差0.12%接近真实值。这个结果很能说明问题GM(1,1)的趋势拟合已经不错但误差仍然偏大加入马尔可夫修正后恰好把残差里那部分系统性正偏差修正掉了。如果你在自己的数据上遇到修正后误差反而变大的情况优先检查残差状态数和历史样本量是否匹配这是我调模型时最常遇到的现象。4.2 评价指标与误差对比做预测模型不能只靠眼睛看曲线需要量化指标。我习惯同时输出三个指标平均绝对误差MAE、平均绝对百分比误差MAPE、均方根误差RMSE。function [mae, mape, rmse] calc_error(y_real, y_pred) e y_real - y_pred; mae mean(abs(e)); mape mean(abs(e ./ y_real)) * 100; rmse sqrt(mean(e.^2)); end对比两个模型时建议把历史拟合误差和样本外预测误差分开算。样本内误差能反映模型“学习”能力样本外误差才是真实预测能力的体现。GMCM的历史拟合误差通常会因为残差被完全修正而接近0这没什么意义真正值得看的是向前一步的样本外预测误差。4.3 GMCM模型适用性检查用GMCM之前建议先对原始数据做灰色模型的级比检验这是很多初学者会跳过的步骤。级比定义为相邻两点比值[ \lambda(k) \frac{x^{(0)}(k)}{x^{(0)}(k-1)} ]可容覆盖区间为[ (e^{-2/(n1)},\ e^{2/(n1)}) ]如果数据点数 (n12)这个区间大约在 ((0.857, 1.167)) 左右。只要级比全部落在区间内就可以放心用灰色模型做主线。检查代码lambda x0(2:end) ./ x0(1:end-1); n length(x0); lb exp(-2/(n1)); ub exp(2/(n1)); if any(lambda lb | lambda ub) warning(数据级比不在可容覆盖区间灰色预测精度可能下降); end如果数据波动太大级比检验不过建议先对数据做平滑处理或考虑改用其他预测方法别硬套灰色模型。5. 实操中踩过的坑和解决方案5.1 状态区间边界归属问题状态划分最典型的问题是残差值恰好落在边界上。假设残差最大值正好等于划分边界如果两个相邻状态都是闭区间这个样本会被同时计入两个状态转移概率就乱了。我的解决方法是统一采用左开右闭区间并且把首尾边界扩展到正负Inf。需要注意的是MATLAB的linspace在边界值上加了个极小阈值做保护防止浮点精度导致边界值无法归入任何状态。5.2 转移矩阵稀疏与NaN问题当历史数据只有10个点、状态数却设为5时很多转移对一次也没出现过转移矩阵会出现整行全0导致后面用max(P(curState, :))取状态时出现问题。我的处理是如果某个状态在历史中完全没出现就给它赋自转移概率1相当于这个状态一旦进入就保持在原地。这只是一种工程上的兜底策略。更严谨的做法是拉普拉斯平滑给每个转移计数加一个很小的常数避免概率为0P(i, j) (n_ij 0.1) / (numel(idx) 0.1 * nStates);5.3 预测步数越长偏差越大马尔可夫修正是有记忆长度的。它预测未来第1步时用的是当前真实状态准确度最高预测第2步时使用的已经是“预测出来的状态”误差开始累积到5步以后修正效果基本退化甚至可能比不修正更差。我在项目里的标准做法是GMCM只做1~3步短期预测。如果需要预测未来5年以上就把多步预测改为滚动更新每预测出一年就把该年作为已知值重新建模再预测下一年。这样虽然增加了计算次数但误差不会滚动放大。5.4 中文乱码与脚本编码问题用App Designer时按钮文字和注释都包含中文部分MATLAB版本在跨平台打开或拷贝代码时会出现中文乱码。我的经验是代码文件一律另存为UTF-8编码函数名和变量名全部用英文只有界面上显示的文字才用中文。这样即使注释乱码也不影响程序运行界面文字由App Designer自动管理编码反而不容易乱。另外从Excel或外部文件导入人口数据时Excel里的数值如果是文本格式readtable读进来会变成cell数组直接str2double转换会得到NaN。稳妥的做法是在Excel里先把列格式改成数值或者用readmatrix直接读数值矩阵避开类型转换的坑。结尾想说的这套GMCM模型我已经在好几个不同场景下跑过人口数据、区域GDP、旅游人数都试过。总的感觉是马尔可夫修正并不是万能药它的价值集中体现在那些“趋势明确但残差有规律可循”的数据上。如果你的数据残差完全随机那GMCM不会带来明显提升但如果是人口这种受政策惯性、统计口径、经济周期影响的数据残差往往带有明显的连续性和转移规律马尔可夫修正就能派上大用场。建模的时候我始终建议先画图、算级比、做样本外验证把每一步的中间结果都摊开来看而不是只盯着最终的MAPE值。模型能解释清楚比模型跑得漂亮更重要这也是这几年做预测项目最大的体会。希望这篇文章里的程序框架能给你的项目省点时间踩过的那些坑你也能躲着走。本文还有配套的精品资源点击获取
返回列表