简介:一套面向MATLAB用户的EOF与REOF分析代码包,为气象、海洋及地学领域研究者提供降维与时空模式提取的参考实现。压缩包共4个文件,均为.m脚本,包含EOF.m、REOF.m、SVD.m与CCA.m,整套仅4KB,代码紧凑、逻辑清晰,便于阅读与按需修改。资源已有323人学习,体现了实际使用价值。内容覆盖EOF分析的数据预处理、SVD分解、主成分提取,以及REOF的空间子区域划分与结果合并流程,同时给出CCA典型相关分析代码,可帮助初学者快速掌握这些常用统计算法的MATLAB实现细节,减少从公式到代码的转化成本;代码结构清晰,适合在科研教学与论文复现中直接借鉴。
1. 为什么 REOF 比 EOF 更值得跑通
很多做气候诊断、信号分解或故障数据分析的人,第一次拿到压缩包里的eof.m、reof.m时,都以为 REOP 只是 EOF 加了一个“旋转”选项,跑完一看图,发现两个模态长得差不多,于是又关掉。实际上,EOF 出来的空间型是纯数学正交基,它强制每个模态与前面所有模态在空间上不相关,这种约束对物理现象并不自然。REOF 通过旋转载荷矩阵,让空间结构更集中、更接近“局部相关”的块状分布,因此在解释物理机制时通常比 EOF 更靠谱。本文直接从奇异值分解(SVD)和因子旋转出发,把 EOF 到 REOF 的完整计算步骤用 MATLAB 写透,适合需要处理海温、气压、振动等多通道时间序列的从业者。
2. 从 EOF 到 REOF:SVD 分解与旋转的数学基础
2.1 EOF 的求解:用 SVD 代替直接算协方差矩阵
EOF 的本质是对方差最大的线性组合进行提取。给定距平场矩阵X,维度为空间点数 m × 时间长度 n,我们要找一组正交空间向量,使投影方差最大。传统做法是计算空间协方差矩阵C = X * X' / (n-1),然后做特征值分解。但直接构造C需要注意数值稳定性,尤其是当m很大而n较小时,协方差矩阵容易产生舍入误差,并且存储成本高。
更常见的做法是直接对X做奇异值分解:
[U, S, V] = svd(X, 'econ');这里U是m × r列正交阵,每一列就是 EOF 空间型;S是对角阵,对角元素是奇异值;V是n × r列正交阵,对应时间方向。因为奇异值分解天然把方差权重带到S中,所以时间主成分通常取PC = V * S,其时间方差为diag(S).^2 / (n-1)。如果只取前k列,就得到前k个 EOF 和主成分。
为什么不直接eig(X*X')?因为X*X'的行列式条件数可能是原始X的平方,奇异值分解利用了双边正交变换,截断到前k个奇异值时误差更可控。尤其在气象海洋数据中,空间维度经常是几万,时间长度只有几百,此时用svd(X, 'econ')只计算min(m,n)个奇异向量,效率远高于直接eig大矩阵。
2.2 正交约束与空间局部化矛盾
EOF 的空间模态必须满足两个条件:第一,方差最大;第二,与其他模态不正交。这意味着第一模态抓住了全场最强的共同变化,但第二模态为了在剩余方差里继续找“最大”,往往会形成大范围的同号或异号结构,看起来像整片海洋都在动。这在物理上常表现为一个模态的数值在同一个大区域内部符号一致,没有明显的局部中心。
问题在于,真实物理过程往往有区域性。例如太平洋海温异常受赤道中东太平洋和北太平洋两块机制控制,这两块区域中间可能没有强相关,但 EOF 的第二个模态会强行用一套正交基去拟合这种“区域不相关”,导致模态互相污染。REOF 的思路是先计算前k个 EOF,保持原始降维不变,然后允许这些空间模态在内部做旋转,不再要求与其他模态空间正交,只要求旋转后的载荷阵在空间上尽可能“稀疏”。
这个“稀疏”不是L0稀疏,而是让每个模态的载荷在少数区域很大,在其他区域接近零。具体做法是对载荷矩阵做正交旋转,常用的旋转方法是 Varimax,它最大化载荷方差,使每个模态的载荷值两极分化。
2.3 REOF 旋转的核心:旋转矩阵与载荷
假设原始 EOF 空间矩阵为E(m × k),特征值对角阵为L = diag(λ_1, ..., λ_k),载荷矩阵定义为:
A = E * sqrt(L)对A做旋转,找到正交矩阵T,使得旋转后载荷阵B = A * T的列之间的方差最大。Varimax 的优化目标是让每个变量在各因子上的载荷平方的方差之和最大,也就是让某些载荷尽量大,其余尽量小。Promax 则在 Varimax 基础上允许因子之间有一定相关性,适合物理上确实存在相互影响的信号。
旋转后的空间模态可以直接用B的列表示,也可以把B再除以sqrt(L)还原到原始 EOF 空间尺度。气象领域更常见的是直接分析旋转载荷,因为它的数值代表格点值与旋转主成分的相关/方差贡献,物理含义更直观。时间系数通常不用原始PC * T,而是通过旋转载荷对原场做回归得到,可以在 MATLAB 里用X' * B近似,具体实现见下一章。
| 方法 | 优化目标 | 因子相关性 | 适用场景 |
|---|---|---|---|
| Varimax | 载荷平方方差最大 | 保持正交 | 数据中主导模态相互独立 |
| Promax | Varimax 后做斜旋转 | 允许相关 | 物理过程存在传递、反馈 |
| Quartimax | 行/列载荷分布简化 | 正交 | 强调空间局部化,但易出现全局因子 |
3. 在 MATLAB 里手写 eof.m 和 reof.m 的最小可用版本
3.1 数据预处理:去均值与缺失值处理
REOF 对输入矩阵非常敏感,输入前必须做距平处理。常见做法是沿时间方向去均值:对每个空间格点,用该格点所有时间的平均值减去原序列。如果数据是 NetCDF 或 HDF5,还要先看经纬度维度顺序,确保重排成(空间点, 时间)的矩阵。
对于NaN,最简单且不引入空间插值误差的做法是删除包含缺失值的时间列。写成函数:
function [X, validT] = preprocess_reof(X) % X: grid × time,空间点在行,时间在列 validT = ~any(isnan(X), 1); X = X(:, validT); % 按空间点去均值 X = X - mean(X, 2); end参数说明:any(isnan(X), 1)沿行方向检查每个时间点是否有NaN,只要某一列存在缺失,就丢掉整个时刻。这种做法适合数据缺失比例很低的情况;若缺失空间范围大,可以考虑插值或使用迭代 EOF,但一小段最小版本代码不值得引入复杂算法。
3.2 实现 eof.m
常见压缩包里的eof.m函数大多接受三个参数:原始场矩阵、要保留的模态个数、是否标准化。这里给出一个 SVD 方法的最小实现:
function [eofs, pcs, lambdas] = eof(X, k) % X: 已去均值的 grid × time 矩阵 % eofs: grid × k,空间模态 % pcs: time × k,标准化的时间系数 % lambdas: k × 1,特征值(空间方差) if nargin < 2 k = 3; end X = X - mean(X, 2); [U, S, ~] = svd(X, 'econ'); var = diag(S).^2 / (size(X, 2) - 1); lambdas = var(1:k); eofs = U(:, 1:k); pcs = (X' * eofs); % 非标准化主成分 % 若需要标准化主成分,可除以 sqrt(lambdas') end逻辑说明:svd(X, 'econ')返回的S对角元素已经按奇异值从大往小排,所以直接取前k列即可。var是每个奇异值对应的时间方差,也就是特征值。计算pcs = X' * eofs得到的是投影系数,它的方差约等于lambdas,但数值量级受网格点数和时间长度影响。后续 REOF 的载荷旋转基于eofs .* sqrt(lambdas')更稳定。
3.3 实现 reof.m
旋转部分我一般调用 MATLAB 自带rotatefactors,它支持 Varimax、Promax 等常用方法,不需要自己写迭代梯度。rotatefactors的输入是载荷矩阵A,注意它按列旋转因子。
function [reofs, rotPC, rotMat] = reof(X, k, method) % 先计算 EOF [eofs, pc, lambdas] = eof(X, k); % 载荷矩阵 loadings = eofs .* sqrt(lambdas'); % 旋转载荷 [rotLoad, rotMat] = rotatefactors(loadings, 'Method', method, ... 'Normalize', true); % 旋转后的空间模态:直接返回旋转载荷 reofs = rotLoad; % 旋转时间系数:用原始场投影 rotPC = X' * reofs; % 将时间系数与旋转模态的符号调成一致 [~, signIdx] = max(abs(reofs)); signVals = sign(reofs(signIdx + (0:size(reofs,2)-1)*size(reofs,1))'); reofs = reofs .* signVals; rotPC = rotPC .* signVals; end参数说明:Normalize设为true可以避免载荷量级差距过大导致旋转偏向高值模态。rotMat是满足正交条件的旋转矩阵。最后一步处理符号翻转是实际问题中非常关键的操作,因为 EOF 和旋转后的模态都可能有正负翻转,手工比图很浪费时间。
3.4 最小调用示例
为了确认两个函数能直接工作,用一个小规模随机场测试:
rng(7); grid = 200; time = 500; % 两个随机信号驱动全场 sig1 = randn(1, time); sig2 = randn(1, time); X = zeros(grid, time); X(1:100, :) = X(1:100, :) + 1.5 * sig1; X(101:200, :) = X(101:200, :) + 1.2 * sig2; X = X + 0.3 * randn(grid, time); k = 2; [eofs, pcs, lambdas] = eof(X, k); [reofs, rotPC, rotMat] = reof(X, k, 'varimax'); figure; subplot(2,2,1); imagesc(reshape(reofs(:,1), 20, 10)); title('REOF1'); subplot(2,2,2); imagesc(reshape(reofs(:,2), 20, 10)); title('REOF2');参数说明:sig1和sig2分别是两个空间区域的驱动信号,噪声加了 0.3 倍标准差。这段代码跑完后,如果旋转正确,第一模态载荷会集中在前1:100格点附近,第二模态载荷集中在101:200附近。如果直接用 EOF,两个模态的空间型会有明显重叠。
4. REOF 步骤里的 3 个关键参数:模态数、旋转方法与收敛容差
4.1 模态数:方差贡献率与 North 检验
REOF 的结果严重依赖保留的 EOF 个数k。选得太多,旋转会出现噪声模态;选得太少,会把多个信号压在一个模态里。常见做法是看方差贡献率:
explained = lambdas / sum(diag(S).^2 / (size(X,2)-1));累积解释方差达到 80% 左右的k是一个起点。但 EOF 特征值存在采样误差,不能只看累计百分比。North 检验给出的经验法则是:如果相邻特征值的误差范围有重叠,说明这两个模态在统计上不可分,不应该同时进入旋转。误差近似为:
δλ = λ * sqrt(2 / n)其中n是有效样本量,不是原始时间长度,而是考虑到时间自相关的滞后长度。我一般会在代码里同时输出特征值和相邻差值:
figure; plot(lambdas, 'o-'); grid on; ylabel('Eigenvalue'); xlabel('Mode index'); text(1:k, lambdas, sprintfc(' %.2f', lambdas));选择平滑点出现拐弯的位置作为k。若用压缩包自带的eof.m,注意函数返回的第三项是不是特征值,有时会写成奇异值而非方差,需要用奇异值平方再除以时间长度。
4.2 旋转方法:Varimax 和 Promax 的差异
第 2 章已经提过,Varimax 是正交旋转,Promax 是斜旋转。在 MATLAB 中切换只需要改method字符串:
reofs_promax = reof(X, k, 'promax');Promax 的输出里rotPC不再是严格正交的主成分,因为因子之间存在相关,投影系数与空间模态的相关性更强。实际项目中,如果只做空间分型,我会先用 Varimax,若发现两个旋转模态的载荷图在关键区域过度重叠,再换 Promax 看一下斜相关矩阵。相关矩阵可以用corr(rotPC)直接计算,若两个 PC 相关系数超过 0.3,说明信号本身耦合很重,Varimax 强行正交会损失原信息。
| 参数 | Varimax | Promax |
|---|---|---|
| 旋转矩阵约束 | 正交 | 斜旋转后存在相关 |
| 迭代目标 | 最大化载荷方差 | 先 Varimax 再斜转 |
| 输出因子相关 | 0 | 非零,可解释过程耦合 |
| 稳定性 | 对噪声更敏感 | 有时因过度拟合而漂移 |
4.3 收敛容差与最大迭代次数
rotatefactors内部有收敛控制,默认容差为1e-6,最大迭代次数为 1000。如果数据量很大或载荷矩阵接近退化,迭代可能不收敛。此时可以显式设置:
[rotLoad, rotMat] = rotatefactors(loadings, 'Method', 'varimax', ... 'Normalize', true, 'Tol', 1e-8, 'MaxIter', 2000);Tol是旋转目标函数变化的阈值,设置过小会导致迭代时间增加,却不一定有明显改善。我的经验是1e-8够用,如果还不收敛,先检查loadings里是否有NaN或全零列,全零载荷会直接影响协方差差求解。旋转矩阵rotMat的行列式应该接近1,如果一个旋转矩阵列向量模长明显偏离1,说明载荷矩阵有问题。
另外要注意:rotatefactors返回的rotLoad只保证方差最大化,不保证每个模态方向一定和原始 EOF 符号一致。实际分析时,我会在旋转后做一个“符号固定”步骤:把每个空间模态绝对载荷最大处的正负统一为正值。如果没有这一步,两次运行结果中的模态图可能会红蓝翻转,导致后续混淆。
5. 排错与验证:从 detect premature eof 到模态漂移
5.1 文件读取与 NaN 导致 SVD 不收敛
有些 RAR 压缩包里除了 MATLAB 脚本,还附带二进制数据文件。用fread读取时,如果文件长度和声明的格式不匹配,常出现“detect premature EOF”的错误提示。这通常意味着数据文件不完整,或者读取的precision类型与实际存储不一致。例如:
fid = fopen('data.bin', 'rb'); data = fread(fid, [m n], '*float32');如果文件整体少了一个记录,fread会报detect premature EOF。解决办法是先检查文件大小:
info = dir('data.bin'); expectSize = m * n * 4; if info.bytes < expectSize error('文件不完整'); end还有一个更隐蔽的坑:矩阵中存在少量NaN,svd会直接崩溃或返回全NaN。不要尝试用mean(X,2)去均值时把NaN也带进去,应该先在preprocess_reof里删除无效列,或者用线性插值补齐。空间维度大而缺失点零星时,我会优先插值,因为删除时间列会让相邻样本失去连续性。
5.2 EOF 符号翻转与旋转后方差重分配
EOF 的符号不确定性不只在画图时影响配色,还会影响 REOF 旋转后的载荷方向。原因是svd返回的U和V符号可以同时翻倍,导致同一套数据在不同 SPD 实现下得到正负相反的模态。REOF 旋转目标函数对符号不敏感,但两个符号相反的载荷在迭代后可能收敛到不同局部极值,形成模态漂移。
验证方法很简单:对同一个矩阵运行两次eof.m,比较前k个模态绝对载荷的最大位置是否一致。只要最大位置一致,符号翻转不影响物理解释。如果最大位置不一致,说明k选得不稳定,需要减小k或使用更稳健的 SVD 算法。
5.3 用合成数据验证 REOF 恢复能力
与其在真实数据上反复调参,不如先用已知局地结构的合成场做验证。我经常用下面这个模板生成两个局地模态:
gridX = 10; gridY = 10; time = 1000; idxA = zeros(gridX, gridY); idxA(2:4, 2:4) = 1; idxB = zeros(gridX, gridY); idxB(6:8, 6:8) = 1; s = randn(time, 2); X = 0.2 * randn(gridX*gridY, time); for t = 1:time X(:, t) = X(:, t) + 2 * s(t,1) * idxA(:) + 1.5 * s(t,2) * idxB(:); end k = 2; [reofs, ~, ~] = reof(X, k, 'varimax'); r1 = reshape(abs(reofs(:,1)), gridX, gridY); r2 = reshape(abs(reofs(:,2)), gridX, gridY); disp(mean(r1(idxA==1))); disp(mean(r1(idxB==1))); disp(mean(r2(idxA==1))); disp(mean(r2(idxB==1)));参数说明:idxA和idxB是空间上的两个 3×3 方块,分别有独立时间信号。如果 REOF 没出问题,第一模态在区域 A 的均值会明显大于区域 B,第二模态反之。若两个均值接近,说明旋转没有分离两块信号,优先检查k是否取太大,或者载荷矩阵是否被标准化过度。
6. 让 REOF 脚本跑进批处理管线:NC 循环与地图输出
真实项目中很少只处理一个时间场,更多是一次读入多个 NetCDF 文件,每个文件里有不同的变量或时间片段。我会把reof.m封装成一个只处理单个变量的函数,然后在主脚本中用dir循环调用:
files = dir(fullfile('data', '*.nc')); for i = 1:length(files) filename = fullfile(files(i).folder, files(i).name); X = ncread(filename, 'sst'); X = reshape(X, size(X,1)*size(X,2), size(X,3)); X = flipud(X); % 如果需要让纬度从小到大 [reofs, rotPC] = reof(X, 3, 'varimax'); base = strrep(files(i).name, '.nc', ''); save(sprintf('out/reof_%s.mat', base), 'reofs', 'rotPC'); end把结果统一保存为.mat文件,后续画图时不用重新跑分解。注意ncread返回数组的维度顺序是(纬度, 经度, 时间),需要先重排为(grid, time)。如果数据文件较大,建议在读取时用NC_DOUBLE类型,避免单精度误差进入 SVD。
输出地图时,我习惯把旋转载荷直接画成imagesc并叠加经纬度网格:
figure; imagesc(lon, lat, reshape(reofs(:,1), nx, ny)'); set(gca, 'YDir', 'normal'); colorbar; title(sprintf('REOF1 - %s', base)); print('-dpdf', sprintf('fig/reof1_%s.pdf', base));这里reshape需要nx*ny == size(reofs,1)。若网格不是规则经纬度,比如三角形网格或不规则区域,先插值到规则网格再分解。批处理时注意把生成文件名的顺序固定,避免不同机器上dir返回顺序不一致,最好在循环前对files做一次按名称排序:[~, idx] = sort({files.name}); files = files(idx);。这段代码放进入口函数后,每次运行只改数据目录就能复用整套 REOF 流程。
本文还有配套的精品资源,点击获取