GRACE数据缺月太头疼?SSA+MATLAB插值实现全攻略 简介本资源是一套面向地球物理与水文遥感研究者的GRACE Mascon数据缺失月份插值工具包聚焦于利用奇异谱分析SSA算法实现时间序列重建适用于科研人员及研究生开展区域陆地水储量变化分析。压缩包共14个文件11个MATLAB脚本、1个说明文档、1个NetCDF测试数据、1张结果示意图总大小71.53MB涵盖数据预处理如decyear、leapyear、SSA核心插值fun_SSA_filling_a/b、ssa_missing_iterative、时空统一uniform_time、可视化gmt_plot等完整流程模块所有代码可直接运行并附简易操作指引。已有506人学习下载提供从原始GRACE月均重力场数据读取、缺失识别、迭代SSA填补到结果绘图的一站式MATLAB实现特别适合作为GRACE数据处理入门实践或方法对比基准亦可作为神经网络等高级插值方案的参照基线。 做GRACE数据处理的人十有八九都遇到过这个场景数据下载下来时间序列画出来本该连续的曲线中间突然断了一截。GRACE任务从2002年运行到2017年期间出现过好几次比较明显的缺测——2011年春、2016年秋以及2017年下半年开始的大段欠测GRACE-FO接棒之后也没完全省心个别月份同样存在数据空洞。缺月一多趋势计算、季节分解、EOF分析全都会受影响这时候就得对缺失月份做插值。处理这类问题我强烈推荐奇异谱分析SSASingular Spectrum Analysis尤其适合GRACE这种以年周期、半年周期加趋势为主的地球物理时间序列配合MATLAB实现整套流程操作直观、效果可控、代码量也不大。这篇文章我会把完整思路和可复现的MATLAB代码都写出来包括SSA的原理拆解、具体参数怎么定、缺测怎么迭代填补、验证实验怎么做还有一堆我实际踩过的坑。适合刚开始接触GRACE数据处理的研究生也适合想把手头缺测时间序列处理利索的工程师参考。1. GRACE数据处理的真实痛点为什么缺月让人头疼1.1 GRACE卫星与数据产品GRACE是NASA和德国航空航天中心联合发起的重力卫星任务2002年3月发射2017年10月正式退役。它通过两颗卫星之间的微波测距变化反演地球重力场的时空变化最经典的应用就是监测陆地水储量变化——地下水、土壤水、湖泊水库、冰川融水的变化都能在大尺度上被它称出来。继任者GRACE-FO在2018年5月接棒延续了这套观测体系。数据处理领域大家常用的产品分两类。一类是Level-2球谐系数也就是GSM文件给出每个月的球谐系数Stokes系数NASA的CSR、JPL、GFZ三家机构各自解算这也是最传统的产品需要自己做滤波、去条带处理另一类是Level-3网格产品比如CSR Mascon、JPL Mascon直接把结果整合到网格上以等效水高EWH表示很多研究直接拿来做区域分析。不管哪种产品都要按照时间序列来使用这就绕不开缺月问题。1.2 缺测月份是怎么产生的GRACE的缺测原因其实很朴素卫星寿命末期电池容量下降加上仪器保护性关机经常导致整月无数据。我记得比较清楚的有几段2011年3月到5月由于设备调校和电池问题连续缺了几个月2016年9月到10月也出现过一次明显中断2017年8月之后到任务结束前数据基本处于时断时续状态。GRACE-FO在2023年也有过短期缺测具体月份还需要查看月度产品列表。时间段大致缺测情况主要原因2011年3月—5月连续月度缺失加速度计异常、电池退化2016年9月—10月月度缺失电池容量不足2017年8月—2018年4月大段欠测卫星寿命末期状态不稳定2023年GRACE-FO部分月份缺失仪器切换与轨道调整这些缺口对研究的影响远比图不好看严重得多。1.3 缺月对后续分析的影响如果只是画一张全球水储量变化图缺几个月还凑合一旦要做定量分析问题全来了。首先是趋势估计线性趋势对首尾和中断处的数据质量很敏感缺测时段如果正好在极端干旱或者洪水期间趋势可能直接被拉偏。其次是季节循环分解月尺度水文信号有明显的年周期和半年周期缺掉几个月之后用传统办法做季节振幅和相位估计结果会带上系统性偏差。再做EOF或ICA这类空间模态分析时缺测网格会导致协方差矩阵不完整空间模态会被扭曲。所以很多人第一反应就是用线性插值填上不就完了。但线性插值本质上是拿缺测前后的两个点连一条直线GRACE的月尺度信号不是直线里面叠加着趋势、季节振荡、噪声简单线性连接不仅会削掉振幅还会把季节相位拉偏。三次样条稍好一些但容易过冲在信号变化剧烈的地方会凭空造出来一些假的波峰波谷。这也是为什么我更倾向于用SSA这类基于信号重构的方法。2. 奇异谱分析(SSA)为什么适合做缺测插值2.1 核心思想从序列本身里找主旋律SSA的核心思想说白了就是让数据自己说话。它不预设信号形式是线性还是正弦而是通过把一条时间序列转换成一堆延时序号构成的轨迹矩阵再做奇异值分解把序列拆解成若干个可以解释的分量。对GRACE这种主要由趋势项、年周期、半年周期和噪声组成的信号前几个分量通常就抓住了绝大部分能量剩下的分量基本是噪声。用这些主分量重构出的序列就等于把噪声和伪信号滤掉还原出的干净版原始序列。我做了一个很粗糙的类比你在一场露天音乐会现场用手机录了一段音频里面有乐队演奏、也有风声和人群杂音。SSA相当于不是为了把某个乐器单独分离出来而是把整段音乐的主旋律提炼出来把嘈杂的环境声丢掉。GRACE缺测插值要的恰恰是这个主旋律——根据有数据的月份找出信号的核心结构再把它延展到缺测的月份上。2.2 四步流程嵌入、分解、分组、重构SSA的标准流程可以拆成四步我按MATLAB实现的顺序来梳理。第一步是嵌入Embedding。把长度为N的时间序列x(t)按窗口长度L构造成轨迹矩阵。比如L12那就让窗口从第1个月滑到第N-L1个月每一列都是原序列的一个L长度片段。这个矩阵是典型的Hankel结构副对角线上的元素都相等。轨迹矩阵的维度是L行、K列其中KN-L1。第二步是分解。对这个轨迹矩阵做奇异值分解SVD得到左奇异向量、奇异值和右奇异向量。奇异值从大到小排列每个奇异值对应一个特征模态代表序列中不同重要程度的子信号。奇异值越大这个模态对原始序列的贡献越大。举个数字上的直观感受对一段干净的GRACE月尺度序列第一个奇异值可能对应趋势第二、第三个一起对应年周期第四、第五个一起对应半年周期后面的奇异值迅速衰减到噪声水平。第三步是分组。把所有模态分成信号组和噪声组。这一步看起来主观但只要画出奇异值曲线和各个模态的形态信号和噪声的分界通常很明显。GRACE的应用里我一般把前5到8个模态划为信号组具体怎么取舍后面第4章详细讲。第四步是对角平均Diagonal Averaging重构。把信号组的模态叠加起来还原出一条长度为N的时间序列。轨迹矩阵经过重构后副对角线上的值通常不完全相等对角平均就是把同一副对角线上的值取平均从而恢复成合法的时间序列。这一步得到的序列就是SSA重构后的平滑信号。2.3 与常规插值方法的对比直接说结论SSA在GRACE缺测插值上比线性插值和三次样条都稳。线性插值的问题在于它只用了缺测点两侧两个点的信息完全没有利用整条序列的季节规律三次样条稍微聪明点但样条曲线在数据波动大的地方容易出现过冲插出来的波形会超过真实信号的合理范围。SSA用的是整条时间序列的时域结构它把趋势、季节振荡这些有物理意义的成分提取出来再借这些成分去推断缺测位置的值所以结果更平滑、更贴合GRACE信号本身的特性。更直观的对比可以看这张表方法是否需要先验模型季节信号保留能力抗噪声能力适用缺测比例线性插值否弱差低10%三次样条否中中中10%-20%ARIMA/卡尔曼滤波是中中中SSA迭代填补否强强高20%-30%当然SSA不是万能的。连续缺测时间太长比如一下子缺了一整年序列的结构信息就不够了任何单序列方法都插不准。这种情况建议结合空间信息比如用周边网格的时空协方差来做或者直接用多变量SSAMSSA把多个网格一起纳入分解。标题里问的基于MATLAB的方式我认为最合理的路线是先用SSA做单点序列的迭代填补再配合一定的空间平滑来约束结果。3. MATLAB完整实现从零写一个SSA插值工具3.1 数据读入与预处理假设你已经把GRACE数据整理成了每条网格一条时间序列保存成CSV或TXT文件第一列是年份带小数第二列是等效水高缺测位置用NaN表示。读取代码很简单% 读取GRACE网格时间序列 data readmatrix(grid_ts.csv); t data(:, 1); % 时间轴year.fraction比如2002.25 x data(:, 2); % 等效水高单位cm % 先画原始序列看缺测在哪里 figure; plot(t, x, o-); xlabel(时间 (year)); ylabel(EWH (cm)); grid on;预处理阶段我会做两件事一是把序列转为N×1列向量避免尺寸问题导致报错二是做标准化处理减去均值除以标准差让数值范围稳定在合理区间。标准化不会改变信号结构但能让后面SVD的数值条件更好尤其在数据量纲差异大的时候帮助明显。需要特别提醒GRACE的等效水高时间序列通常不需要额外去趋势再插值因为SSA本身会把趋势作为主成分之一提取出来。如果提前去趋势反而可能把低频信号和年周期的低频尾瓣混在一起给分组增加麻烦。3.2 SSA核心函数的MATLAB实现我习惯把SSA拆成两个函数一个负责嵌入和分解一个负责重构。嵌入函数如下function [U, S, V] ssa_decompose(x, L) % SSA分解构建轨迹矩阵 SVD % 输入: x - N×1时间序列 % L - 窗口长度 % 输出: U,S,V - svd()返回的分解结果 x x(:); N length(x); K N - L 1; % 构建轨迹矩阵Hankel矩阵 Y zeros(L, K); for i 1:K Y(:, i) x(i : i L - 1); end % 奇异值分解 [U, S, V] svd(Y, econ); end这段代码里svd(Y, econ)用的是经济型分解矩阵规模大时能省不少内存。注意svd返回的S是对角矩阵真正用的时候要取S(i,i)拿到第i个奇异值。重构函数复杂一点核心是选定的模态叠加和对角平均function x_rec ssa_reconstruct(U, S, V, group) % SSA重构选取group中的模态叠加后对角平均 % 输入: U,S,V - ssa_decompose的输出 % group - 要保留的模态编号例如 [1 2 3 4 5] % 输出: x_rec - 重构后的时间序列 L size(U, 1); K size(V, 1); N L K - 1; % 将选中的模态叠加成重构轨迹矩阵 Y_rec zeros(L, K); for i group Y_rec Y_rec S(i, i) * (U(:, i) * V(:, i)); end % 对角平均 x_rec zeros(N, 1); cnt zeros(N, 1); for l 1:L for k 1:K x_rec(l k - 1) x_rec(l k - 1) Y_rec(l, k); cnt(l k - 1) cnt(l k - 1) 1; end end x_rec x_rec ./ cnt; end对角平均那段代码是新手最容易写错的地方很多人直接用mean(Y_rec, 2)但这只对第一列和最后一列正确中间位置的元素不止一个。正确做法就是遍历所有(l,k)把贡献累加到对应的时间索引上最后除以计数。这个细节决定了重构序列的边界和中间段是否正确。3.3 迭代填补主流程有了分解和重构核心的迭代填补逻辑反而简单。我的实现思路是先用三次样条做一次初始填充让整条序列连续然后对填充后的序列做SSA分解重构得到干净版信号再用干净版信号替换掉原始缺失位置的数值接着用更新后的序列重复SSA分解重构直到缺失位置的值收敛。这个流程本质上是EM算法的一种应用初始估计缺失值然后用信号模型修正估计反复迭代。function x_filled ssa_fill_gap(x, L, group, max_iter, tol) % SSA迭代填补缺测 % 输入: x - 原始序列含NaN % L - 窗口长度 % group - 信号模态组 % max_iter - 最大迭代次数 % tol - 收敛阈值 % 输出: x_filled - 填补后的完整序列 x x(:); N length(x); nan_idx isnan(x); if ~any(nan_idx) x_filled x; return; end % 标准化后面再还原 mu mean(x(~nan_idx)); sd std(x(~nan_idx)); x_norm (x - mu) ./ sd; % 初始填充三次样条 t 1:N; x_filled x_norm; x_filled(nan_idx) spline(t(~nan_idx), x_norm(~nan_idx), t(nan_idx)); % 迭代 for iter 1:max_iter % SSA分解与重构 [U, S, V] ssa_decompose(x_filled, L); x_rec ssa_reconstruct(U, S, V, group); % 检查缺失位置的变化量 delta max(abs(x_filled(nan_idx) - x_rec(nan_idx))); % 用重构值替换缺失位置 x_filled(nan_idx) x_rec(nan_idx); if delta tol fprintf(迭代 %d 次收敛delta %.6f\n, iter, delta); break; end end % 还原尺度 x_filled x_filled * sd mu; end这个函数用起来很直观把含NaN的序列传进去设置窗口长度L、分组group、最大迭代次数和容差返回补全后的序列。我自己常用的参数组合是L24group1:5max_iter100tol1e-4。为什么L取24而不是12后面会详说。3.4 模拟缺测实验效果怎么验证插值到底靠不靠谱不能靠肉眼拍板。最稳的办法是模拟试验拿一条没有缺测的完整序列人为挖掉几个月再用SSA插值把插值结果与真实值对比算出误差。这一步强烈建议在正式处理数据前先做一遍能帮你确认参数组合是否合理。% 模拟试验假设完整序列为 x_full rng(42); miss_idx 20:22; % 人为挖掉第20到22个月 x_test x_full; x_test(miss_idx) NaN; % 用SSA填补 x_filled ssa_fill_gap(x_test, 24, 1:5, 100, 1e-4); % 评估 rmse sqrt(mean((x_filled(miss_idx) - x_full(miss_idx)).^2)); r corr(x_filled(miss_idx), x_full(miss_idx)); nse 1 - sum((x_full(miss_idx) - x_filled(miss_idx)).^2) / ... sum((x_full(miss_idx) - mean(x_full(miss_idx))).^2); fprintf(RMSE %.3f cm, R %.3f, NSE %.3f\n, rmse, r, nse);我拿某流域网格的实测序列做过一次模拟试验连续挖掉三个月线性插值的RMSE大概有3.2cmSSA插值的RMSE压到了1.1cm左右NSE从0.6提高到0.93。这个效果差异主要来自SSA对季节信号的保留GRACE月序列里年周期的振幅有十几厘米线性插值跨过3个月时直接把一个波峰削平了而SSA能根据前后完整周期的相位把这个波峰重建出来。4. 实战踩坑记录与参数调优心得4.1 窗口长度L怎么选窗口长度L是SSA里最重要的超参数它决定了能识别的最长周期。理论上L至少要大于目标周期通常取目标周期的一半就能识别但为了稳定我会取主周期的2到3倍。GRACE月尺度序列的主周期是12个月所以L24或36都很常见如果序列里还有明显的半年周期L太小就分不开半年和年周期的模态如果L太大比如96个月轨迹矩阵的K值就很小模态估计的样本量不足重构边界效应也更重。我个人的经验公式是L取主周期的2倍到3倍之间序列整体长度月数的1/5以下。GRACE任务月序列总共160多个月L24或36都安全。如果数据是GRACE-FO和GRACE拼接的长序列甚至可以考虑L48但这时候要重点观察模态分离是否合理。4.2 group分组怎么定才科学分组是最容易翻车的环节很多人习惯前5个全保留但对不同流域、不同时间段的GRACE序列前几个模态的分布并不一样。我通常的做法是画出奇异值曲线和各模态的特征向量先看能量集中在哪figure; subplot(2,1,1); plot(log10(diag(S)), o-); xlabel(模态编号); ylabel(log10(奇异值)); title(奇异值谱); subplot(2,1,2); for i 1:min(6, length(diag(S))) plot((1:size(U,1)) i*2, U(:,i) i*2, LineWidth, 1.2); hold on; end xlabel(窗口内时间); ylabel(左特征向量偏移显示); legend(arrayfun((i) sprintf(模态%d, i), 1:min(6, length(diag(S))), UniformOutput, false), Location, best);观察重点有两个。一是奇异值曲线在某个编号之后开始平缓那个拐点之后基本就是噪声二是特征向量的形态如果某对特征向量看起来像正弦和余弦周期大约12个月那它们就是年周期模态必须保留。实际使用中趋势模态通常是第1个年周期是一对模态半年周期是另一对所以group选择1:5或1:7都合理。如果你看到第3、4个模态的频率并不是12个月或6个月而是混杂的那就要考虑L是不是选小了。另外提醒一下SSA的模态经常成对出现这是因为一个余弦波在SVD下会被分解为相位正交的两个模态。判断配对不能只靠奇异值接近还要看特征向量的相关性两个模态的特征向量做相关如果相关系数很高且形态相差90度相位基本就是一对。保留时必须成对保留只留其中一个会破坏信号的幅度。4.3 边界效应与迭代收敛问题SSA重构有个天然弱点序列首尾附近的估计误差偏大。原因在于轨迹矩阵两端覆盖的样本少对角平均时的计数也少所以首尾几个月与真实信号的拟合度差一些。放在插值场景里意味着如果缺月正好落在序列开头或结尾插值效果会打折扣。处理办法是缺测靠近边缘时尽量多保留两端的数据长度或者用更长的L把边界信息拉进来一点。如果你要插值的位置是2017年底那一段而序列在2018年6月就结束结果就要打个问号。迭代收敛方面我遇到过迭代震荡的情况缺失位置的值在重构和被重构之间来回跳始终不收敛。这通常是分组里混入了噪声模态导致的。把group缩小只保留能量最集中的几个模态震荡基本就消失了。如果一定要保留较多模态可以给更新加松弛因子每次只更新缺失位置的一部分x_filled(nan_idx) alpha * x_rec(nan_idx) (1-alpha) * x_filled(nan_idx)alpha取0.5到0.8。4.4 逐网格批量处理与性能优化GRACE网格数据有成千上万个网格逐网格跑SSA虽然能出结果但纯for循环会很慢。我实测一个网格一次分解重构大概几十毫秒几万个网格就要好几个小时。优化思路有两个。一是只处理陆地区域的网格海洋网格直接跳过。用GRACE的mask文件筛选一下能省掉一半以上的计算量。二是把最外层的网格循环改成parfor并行循环。MATLAB的parfor对这类每个循环独立的任务提升非常明显我的工作站12个内核处理速度能快8到10倍。需要留意的是matlabpool里要保证每个worker都能访问到函数文件路径问题提前配好。parfor i 1:numel(grid_list) x all_data(i, :); x_filled ssa_fill_gap(x, 24, 1:5, 100, 1e-4); all_filled(i, :) x_filled; end还有一个更进阶的优化方案如果内存充足可以对所有网格组成二维矩阵做多变量SSAMSSA把空间相关性和时间结构联合起来建模。不过MSSA的轨迹矩阵会膨胀得厉害内存不够时反而得不偿失。对大多数GRACE处理需求单序列SSA逐网格并行已经够用了。数据量大时还有个小技巧预处理阶段把序列做一次粗差检测极端异常值先剔除再进SSA。GRACE某些月份的解算可能存在明显野值如果不去掉SVD的奇异值会被野值拖偏principal component直接变形。我在实际数据处理中还有一点体会SSA插值完成后不要直接认为万事大吉最好把插值结果叠加到原始序列上画一张完整图重点检查缺测位置是否存在突兀跳变。如果某个缺月插出来的值和前后月份衔接得很生硬多半是分组没选好或迭代没收敛。另外做区域平均时建议把多个网格的插值结果再做一次空间平滑消除单点插值的随机误差。这套流程跑下来GRACE的缺月基本不会成为后续分析的拦路虎。最后分享一个小技巧SSA的参数组合在不同流域表现不完全一致正式处理前先用模拟缺测实验测试两三组参数挑RMSE最小的一组比迷信任何固定组合都靠谱。本文还有配套的精品资源点击获取