拿到一个站点几十年的降雨量序列,第一步干什么?绝大多数人会直接画个折线图看个大概,然后就开始写结论。但要是准备发论文、写报告,光靠肉眼可不行,趋势是否显著、周期是否真实存在,这些都得有统计检验支撑。我常用的组合拳就是MK检验(Mann-Kendall趋势检验)加Morlet小波分析,前者判断降雨量在几十年尺度上是增是减、是否显著,后者看不同周期尺度的振荡特征随时间怎么变化。这篇就把整个流程和Matlab代码完整拆开讲清楚,新手照着抄也能跑通。
先说个我的结论:这两个方法搭配起来,基本是水文气候分析里最实用的组合之一。MK检验不需要数据满足正态分布,对少量异常值不敏感,适合降雨这类偏态明显的变量;Morlet小波则能在时间-频率域同时给出信息,告诉你哪个年代2年周期比较强、哪个年代6年周期占主导。这篇博文的目标读者是水文、气象、地理或生态方向的研究生和科研人员,尤其是手里有一份降雨量数据、想做趋势和周期分析但不太确定怎么下手的同学。我会把原理讲得尽量通俗,把代码注释写到保姆级别,再附上我在实际项目中踩过的数据坑和图片解读经验。
1. 方法原理:先搞懂MK检验和小波到底在算什么
1.1 Mann-Kendall检验:一个只看“相对次序”的稳健裁判
MK检验的英文全称是Mann-Kendall Trend Test,由Mann和Kendall分别提出并完善,是一种非参数检验方法。所谓“非参数”,意思是它不关心数据来自什么分布,不假设总体服从正态或对数正态,而是直接比较序列中每两个数据的大小关系。
我把它的逻辑简化成一句话:把序列里任意两个时间点的数值做比较,统计“后面的比前面的大”和“后面的比前面的小”的次数之差。如果前者显著多于后者,就说明序列有上升趋势;反过来就是下降趋势。
具体计算过程是这样的。对于长度为n的序列x,定义统计量S:
S = Σ Σ sign(x_j - x_i),其中i从1到n-1,j从i+1到n
sign是符号函数,x_j大于x_i时取1,小于取-1,等于取0。如果序列完全随机,S的期望值为0;S为正说明整体上升,S为负说明整体下降。
但光有S不够,还得判断这个S是否显著偏离0。在序列无趋势的零假设下,当n较大时S近似服从正态分布,均值为0,方差为Var(S) = [n(n-1)(2n+5) - Σ t_i(t_i-1)(2t_i+5)] / 18。这里t_i是序列中第i组重复值(相同数值)的个数,这一项是“结修正”,处理数据中有大量相同取值时方差被高估的问题。
然后构造标准化统计量Z:
- 当S>0时,Z = (S-1) / √Var(S)
- 当S<0时,Z = (S+1) / √Var(S)
- 当S=0时,Z = 0
这里的-1和+1是“连续修正”项,把离散分布向正态分布靠拢时的小样本修正。给定显著性水平α(通常取0.05),如果|Z|大于标准正态分布的临界值1.96,就认为趋势显著。
还需要说明一个MK检验容易被忽视的配套指标——Sen斜率。MK检验只告诉你趋势是否存在和是否显著,但没告诉你趋势的幅度有多大。Sen斜率的思想非常朴素:把所有两两组合的斜率取中位数。假设有n个点,任意两点(i, x_i)和(j, x_j)形成一个斜率(x_j-x_i)/(j-i),共有n(n-1)/2个斜率值,取这堆斜率的中位数就是趋势的稳健估计。它比线性回归的斜率更抗异常值,适合降雨这种可能有极端值的数据。
MK检验有一个重要假设:序列内部相互独立。如果降雨量数据存在显著的自相关(比如连续几年的丰枯交替),检验的可靠性会下降。这个问题在后面代码实现部分我会额外提一个处理建议。
1.2 Morlet小波分析:带时间定位能力的“频率放大镜”
小波分析常常被形容成“数学显微镜”,我更喜欢把它比作“能定位时间的频率放大镜”。傅里叶变换能告诉你信号里有哪些频率成分,但给不出这些频率成分是哪个年代出现的——比如一个6年周期,傅里叶变换告诉你它有,但它不告诉你1970年代强还是2010年代强。小波变换通过引入一个窗口函数(母小波)在时间轴上滑动并缩放,同时获得时间和频率两个维度的信息。
Morlet小波是连续小波变换中最常用的母小波,它的实部是余弦震荡乘以高斯包络。用它分析降雨量序列,可以同时提取周期成分的振幅和相位信息,特别是能揭示不同周期成分随时间的演化过程。
Morlet小波在频域中的表达式简洁:
ψ̂(sω) = π^(-1/4) · H(ω) · exp[-(sω - ω₀)²/2]
其中s是小波尺度,ω₀是波数(无量纲频率),通常取6。H(ω)是Heaviside阶跃函数,即负频率处为0。这个设置有两个好处:一是让小波本身满足容许条件(也就是逆变换存在的数学前提),二是ω₀=6的时候,时间分辨率与频率分辨率之间的平衡比较理想。如果ω₀取小一些(比如2),时间分辨率提高但频率分辨率下降,适合分析快速变化的信号;如果取大一些(比如12),频率分辨率高但时间分辨率差,适合精确分辨相近的周期。
小波变换的计算流程说穿了三步:把原始序列做傅里叶变换,把Morlet小波按不同尺度做傅里叶变换,两者在频域相乘后逆变换回时间域。每一组“步骤”都对应一个尺度s,得到的就是该尺度下小波系数随时间的变化。把所有尺度拼起来,就得到“时间-周期”二维矩阵。
这里需要厘清一个新手经常搞混的概念——“尺度”和“周期”不是一回事。尺度s是小波的伸缩参数,周期的物理含义要到变换完成后才能确定。对Morlet小波(ω₀=6),有经验关系T ≈ 1.033s,也就是说尺度s对应约1.033倍s的周期。在绘图时,我们会把纵轴转换成周期,方便读者直接读。
1.3 为什么MK检验和Morlet小波是“黄金搭档”
我的理解是,这两种方法从两个互补的角度回答了两个完全不同的问题。MK检验回答“趋势问题”:这几十年的降雨量整体有没有显著增加或减少?答案是增或减、显著或不显著、增幅多大。Morlet小波回答“周期问题”:降雨序列里存在哪些周期尺度的波动?这些波动在不同年代是强还是弱?
趋势和周期是降雨时间序列两个最核心的属性。城市规划、水资源配置最关心趋势;而农业种植、防旱抗旱关注周期规律的演变。把两者结合,一份降雨数据就能同时提供两方面的结论,这也是为什么水文气候论文里这对组合几乎成了标配。
实际操作中,我一般先跑MK检验,得到趋势方向判断后,再做小波分析,观察去掉趋势后(或保留趋势)的周期结构。如果序列存在明显趋势,小波分析时建议先把趋势剔除,否则趋势能量会泄露到低频段,掩盖真实的周期信号。这个细节后面代码里会体现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Matlab代码实现:从零搭起一套完整分析流程
2.1 数据准备:格式、缺测、标准化一步到位
我的经验是,数据准备阶段踩的坑最多,如果这个环节出问题,后面代码跑得再漂亮也白搭。首先明确输入格式:建议把降雨量序列做成两列,第一列是时间序号(年、月或日序号),第二列是降雨量。时间序号不要求是日期格式,只需要确保等间隔即可。比如月降雨量,1970年1月记为1,1970年2月记为2,依次类推。
等间隔是MK检验和小波分析共同的前提。如果原始数据有缺测,直接跳过会导致时间间隔不相等。处理缺测有几种方式:最简单的是线性插值,适用于少量缺失;如果缺测集中在一个时间段,就要考虑是否用多年同月平均值填补,或者干脆截取连续完整的时段分析。
在实际项目中,我会先用这段脚本来快速检查数据质量:
matlab复制% 读取数据:两列,第1列时间序号,第2列降雨量(mm)
% 示例数据:120个月的月降雨量,前10年为模拟数据
t = (1:120)';
rng(2024); % 固定随机种子便于复现
x = 50 + 30*sin(2*pi*t/12) + 20*sin(2*pi*t/36) + 0.05*t + 10*randn(size(t));
% 检查是否有缺失值
fprintf('数据长度:%d\n', length(x));
fprintf('缺失值个数:%d\n', sum(isnan(x)));
% 检查时间间隔是否均匀
dt = diff(t);
if all(dt == dt(1))
fprintf('时间间隔均匀,间隔为%d\n', dt(1));
else
fprintf('警告:时间间隔不均匀!\n');
end
% 简单可视化
figure('Position', [100, 100, 900, 400]);
plot(t, x, 'b-', 'LineWidth', 1.2);
xlabel('时间(月)');
ylabel('降雨量(mm)');
title('降雨量时间序列');
grid on;
这里我用的是模拟数据,读者换成自己的真实数据即可。真实数据读取可以用load、readmatrix或xlsread,根据文件格式选择。readmatrix在较新版本的Matlab里最省事,能自动识别逗号分隔和Excel表格。
关于标准化:MK检验不需要标准化,因为它只比较相对大小,数值缩放不影响结果。小波分析则建议在分析前对序列去均值,因为均值分量会在小波系数中产生不必要的低频能量。如果是季节特征很强的月数据,还可能需要先做“标准化距平”——用每个月的值减去该月多年平均值再除以该月标准差,这样可以剔除季节循环的干扰。但这一步看具体研究目的,如果就想看原始降雨量的整体周期结构,直接去均值就够。
2.2 MK检验完整代码:统计量、显著性、Sen斜率一网打尽
下面的函数是我在项目中反复用过的版本,处理了“结”(ties)、NaN缺失值和连续修正,输出项完整。核心逻辑清晰,建议直接保存为mk_trend_test.m使用。
matlab复制function [Z, p_value, S, varS, slope, intercept] = mk_trend_test(x, alpha)
% MK_TREND_TEST Mann-Kendall趋势检验(含Sen斜率估计)
% 输入:
% x - 时间序列(列向量)
% alpha - 显著性水平(默认0.05)
% 输出:
% Z - 标准化检验统计量
% p_value - 双尾p值(Z对应)
% S - Mann-Kendall统计量
% varS - S的方差
% slope - Sen斜率(趋势幅度,单位/时间步)
% intercept- 趋势线截距(用于绘图)
if nargin < 2
alpha = 0.05;
end
% 处理缺失值
x = x(:); % 转为列向量
valid = ~isnan(x);
if sum(valid) < 3
error('有效数据点少于3个,无法进行MK检验');
end
x = x(valid);
n = length(x);
% 计算S统计量:双重循环比较每对数据
S = 0;
for i = 1:n-1
for j = i+1:n
diff_ij = x(j) - x(i);
if diff_ij > 0
S = S + 1;
elseif diff_ij < 0
S = S - 1;
end
% diff_ij == 0 时对S无贡献
end
end
% 处理“结”(相同数值)对方差的影响
unique_vals = unique(x);
tie_counts = zeros(length(unique_vals), 1);
for k = 1:length(unique_vals)
tie_counts(k) = sum(x == unique_vals(k));
end
tie_counts = tie_counts(tie_counts > 1); % 只看有重复的组
ties_correction = 0;
if ~isempty(tie_counts)
tie_term = tie_counts .* (tie_counts - 1) .* (2*tie_counts + 5);
ties_correction = sum(tie_term);
end
% S的方差公式
varS = (n*(n-1)*(2*n+5) - ties_correction) / 18;
% 连续修正并标准化得到Z
if varS == 0
Z = 0;
else
if S > 0
Z = (S - 1) / sqrt(varS);
elseif S < 0
Z = (S + 1) / sqrt(varS);
else
Z = 0;
end
end
% 双尾p值
p_value = 2 * (1 - normcdf(abs(Z)));
% Sen斜率估计:所有点对斜率取中位数
slope = [];
N = n*(n-1)/2;
if N > 0
k_vals = zeros(N, 1);
k = 0;
for i = 1:n-1
for j = i+1:n
if x(j) ~= x(i) % 避免除以0
k = k + 1;
k_vals(k) = (x(j) - x(i)) / (j - i);
end
end
end
if k > 0
slope = median(k_vals(1:k));
else
slope = 0;
end
end
% 截距用于绘图:残差中位数
t_axis = (1:n)';
intercept = median(x - slope * t_axis);
% 控制台输出结果
fprintf('===== Mann-Kendall检验结果 =====\n');
fprintf('样本长度 n = %d\n', n);
fprintf('S统计量 = %d\n', S);
fprintf('Var(S) = %.2f\n', varS);
fprintf('Z统计量 = %.3f\n', Z);
fprintf('p值 = %.4f\n', p_value);
fprintf('Sen斜率 = %.4f(单位/时间步)\n', slope);
if slope > 0
fprintf('趋势方向:上升\n');
elseif slope < 0
fprintf('趋势方向:下降\n');
else
fprintf('趋势方向:无明显趋势\n');
end
if abs(Z) > norminv(1-alpha/2)
fprintf('结论:在%.2f水平上趋势显著\n', alpha);
else
fprintf('结论:在%.2f水平上趋势不显著\n', alpha);
end
fprintf('===============================\n');
end
调用例子:
matlab复制x_demean = x - mean(x); % 去均值不是必须的,这里仅演示调用
[Z, p, S, varS, slope, intercept] = mk_trend_test(x, 0.05);
这里我解释几个代码细节的意图。双重循环比较看起来笨拙,但n一般几百个点,计算量完全可以接受。如果你有上万级别的长序列(比如日数据),建议改用矩阵化和向量化写法,否则双重循环会慢到怀疑人生。Sen斜率部分,我提前用N = n*(n-1)/2分配了数组,避免循环中动态增长拖慢速度。最后中位数只取了斜率非零的样本,注意这与有些版本把零点斜率也纳入中位数的做法略有差异,但实际影响很小,我更倾向不把无差异的零斜率点引入趋势幅度估计。
2.3 Morlet小波分析核心函数实现
小波分析的核心函数我整合了Morlet小波变换、尺度-周期转换、影响锥计算、红噪声95%置信水平估算。这个版本深受Torrence和Compo发表于《Bulletin of the American Meteorological Society》的经典论文影响,但我把它重写得更容易读懂,依赖项也控制在Matlab基础函数内,不依赖任何工具箱(除了统计工具箱的chi2inv,老版本Matlab可能需要确认)。
matlab复制function [wave, period, scale, coi, sig95] = wavelet_analysis(x, dt, varargin)
% WAVELET_ANALYSIS 连续小波变换主函数(Morlet小波)
% 输入:
% x - 时间序列(列向量,等间隔)
% dt - 采样时间间隔(月数据dt=1,年数据dt=1,日数据dt=1)
% 'pad' - 是否补零到2的幂,默认1(建议保持默认)
% 'dj' - 尺度离散化步长,默认0.25
% 's0' - 最小尺度,默认2*dt
% 'j1' - 最大尺度指数,默认由数据长度自动确定
% 'mother' - 母小波,默认'morlet'
% 'param' - 波数omega0,默认6
% 输出:
% wave - 小波系数矩阵(n×nscale,复数)
% period - 各尺度对应的Fourier周期
% scale - 实际使用的尺度序列
% coi - 影响锥(1×nscale,边界时间宽度)
% sig95 - 95%置信水平对应的小波功率(n×nscale)
% 参数解析
p = inputParser;
addRequired(p, 'x');
addRequired(p, 'dt');
addParameter(p, 'pad', 1);
addParameter(p, 'dj', 0.25);
addParameter(p, 's0', 2*dt);
addParameter(p, 'j1', []);
addParameter(p, 'mother', 'morlet');
addParameter(p, 'param', 6);
parse(p, x, dt, varargin{:});
x = p.Results.x(:);
dt = p.Results.dt;
pad = p.Results.pad;
dj = p.Results.dj;
s0 = p.Results.s0;
j1 = p.Results.j1;
mother = p.Results.mother;
param = p.Results.param;
n = length(x);
x = x - mean(x); % 去均值,重要!
% ----- FFT准备 -----
if pad
nfft = 2^nextpow2(n); % 补零到2的幂
else
nfft = n;
end
xhat = fft(x, nfft);
% 构造频率向量(与FFT输出顺序对应)
% 范围为(-1/(2dt), 1/(2dt)]
freq = (0:nfft-1) / (nfft*dt);
freq(freq > 1/(2*dt)) = freq(freq > 1/(2*dt)) - 1/dt;
omega = 2*pi*freq; % 角频率
% ----- 构造尺度序列 -----
if isempty(j1)
j1 = fix((log2(n*dt/s0)) / dj);
end
scale = s0 * 2.^((0:j1)*dj);
nscale = length(scale);
% ----- 计算小波系数 -----
wave = zeros(n, nscale);
for a = 1:nscale
s = scale(a);
% Morlet小波在频域的表达
% psi_hat(s*omega) = pi^(-1/4) * exp(-(s*omega - param)^2 / 2) for omega > 0
expnt = -((s*omega - param).^2) / 2;
daughter = pi^(-0.25) * exp(expnt);
daughter(omega <= 0) = 0; % 只在正频率有值
% L1归一化(Torrence & Compo推荐,保持能量与尺度无关)
daughter = daughter * sqrt(2*pi*s/dt);
% 频域相乘并逆变换
wave(:, a) = ifft(xhat .* daughter, nfft);
wave(:, a) = wave(1:n, a);
end
% ----- 尺度转换为Fourier周期 -----
if strcmpi(mother, 'morlet')
% 对Morlet小波,T = 4*pi*s / (omega0 + sqrt(2 + omega0^2))
period = scale * (4*pi) / (param + sqrt(2 + param^2));
else
period = scale; % 其他小波类型按需要修改
end
% ----- 影响锥COI -----
% Morlet小波的e-folding时间为 sqrt(2)*s,换算到时间轴
coi = sqrt(2) * scale * dt;
% ----- 95%显著性水平(红噪声背景谱方法)-----
% 估计lag-1自相关系数
alpha_coef = corr(x(1:end-1), x(2:end));
if isnan(alpha_coef)
alpha_coef = 0;
end
% 红噪声功率谱密度(AR(1)过程理论谱)
Pk = (1 - alpha_coef^2) ./ (abs(1 - alpha_coef * exp(-2*pi*1i*freq*dt)).^2);
siglvl = 0.95;
dof = 2; % 复小波自由度2
chisquare = chi2inv(siglvl, dof) / dof;
variance = var(x);
sig95 = zeros(n, nscale);
for a = 1:nscale
% 找到尺度a中心频率对应的谱密度
f_center = param / (2*pi*scale(a));
[~, idx] = min(abs(freq - f_center));
Pk_scale = Pk(idx);
% 95%置信水平对应的小波功率阈值
sig95(:, a) = variance * Pk_scale * chisquare / dof;
end
end
这个函数的输出中,wave是核心结果,复数矩阵。实部反映信号在不同周期尺度上的振荡相位和幅度,模方(绝对值平方)反映能量强度。period是每个尺度对应的周期,绘图时纵轴可以直接用log2(period)来压缩周期范围。coi是影响锥,用于标识边缘效应区域——这是小波分析新手最容易忽略的东西,我反复跟学生强调:看小波图必须留意影响锥,锥外的能量再强也不可全信。
2.4 一键出图:实部图、模方图、小波方差图、显著性检验
代码有了,数据有了,接下来是出图环节。我一般会同时生成三张图:
第一张,小波系数实部等值线图。这张图能看到周期性振荡在时间-周期平面上的分布。横轴为时间,纵轴为log2(周期),颜色代表实部值,红色正、蓝色负。正负交替的区域说明该时段确实存在这一尺度的周期性波动。
第二张,小波功率谱图(模方等值线图)叠加95%显著性等值线。这张图回答“哪个周期在哪个时段最强”。颜色越亮能量越大,黑色细线围出的区域是95%置信域。
第三张,小波方差图。把小波功率在时间方向求平均,得到一条“能量-周期”曲线,峰值对应的周期就是全时段最显著的主导周期。
matlab复制% ===== 绘图主程序 =====
% 假设已调用 wavelet_analysis 得到 wave, period, scale, coi
% 这里用模拟数据完整演示一遍
clear; close all; clc;
rng(2024);
t = (1:120)';
x = 50 + 30*sin(2*pi*t/12) + 20*sin(2*pi*t/36) + 0.05*t + 10*randn(size(t));
dt = 1;
[wave, period, scale, coi, sig95] = wavelet_analysis(x, dt, 'dj', 0.25);
power = abs(wave).^2; % 小波功率
signif = power ./ sig95; % 显著性比值,>1表示显著
% ---- 图1:小波系数实部等值线 ----
figure('Position', [50, 50, 900, 500]);
contourf(t, log2(period), real(wave)', 30, 'LineColor', 'none');
colormap(jet);
colorbar;
xlabel('时间(月)');
ylabel('周期(月,log2坐标)');
title('小波系数实部等值线图');
ylim([min(log2(period)), max(log2(period))]);
yticklabels(2.^yticks);
hold on;
% 绘制影响锥
plot(t, log2(max(coi)) * ones(size(t)), 'k--', 'LineWidth', 1.2);
hold off;
% ---- 图2:小波功率谱 + 显著性 ----
figure('Position', [50, 50, 900, 500]);
contourf(t, log2(period), power', 30, 'LineColor', 'none');
colormap(jet);
colorbar;
xlabel('时间(月)');
ylabel('周期(月,log2坐标)');
title('小波功率谱(阴影区域为95%显著)');
ylim([min(log2(period)), max(log2(period))]);
yticklabels(2.^yticks);
hold on;
% 叠加95%显著等值线
contour(t, log2(period), signif', [1, 1], 'k-', 'LineWidth', 1.5);
% 影响锥
plot(t, log2(max(coi)) * ones(size(t)), 'k--', 'LineWidth', 1.2);
hold off;
% ---- 图3:小波方差图 ----
% 计算全局小波功率(所有时段的平均)
global_power = mean(power, 1);
figure('Position', [50, 50, 500, 400]);
plot(period, global_power, 'b-', 'LineWidth', 1.5);
set(gca, 'XScale', 'log');
xlabel('周期(月)');
ylabel('小波方差(平均功率)');
title('全局小波方差谱');
grid on;
% 标注峰值
[pks, locs] = findpeaks(global_power, 'SortStr', 'descend');
for i = 1:min(3, length(locs))
text(period(locs(i)), pks(i), sprintf(' T=%.1f', period(locs(i))), 'FontSize', 10);
end
这段绘图代码有几个值得注意的地方。纵轴用log2(period),好处是能把周期性从1个月到100多个月都展示清晰,不至于长周期被压缩成一团。yticklabels(2.^yticks)这行把坐标刻度标签还原成实际周期(比如2、4、8、16、32、64……),阅读起来直观,这是做这个图的小技巧。影响锥的绘制,我取了coi的最大值画了一条水平参考线,其实更严格的做法是画出一条随着时间变化的边界,但对于标准图来说,先画一条全局参考线让读者留意边缘效应就够了。
显著性叠加:signif = power ./ sig95,只要比值大于1(也就是等值线图里值=1的边界),就说明该时刻该尺度上的小波功率超出了红噪声背景的95%置信上限。这个方法在Torrence & Compo框架里是标准做法。
3. 小波图件解读:四张图看穿降雨周期变化
3.1 小波系数实部等值线图:周期存在的“时间-尺度”证据
有图之后,最重要的环节是别画完就完事,要能解读。实部等值线图是我建议人们第一眼看的图。
读图逻辑是这样的:横轴是时间,纵轴是周期(对数刻度),图上每一个像素点的颜色代表某个周期尺度在某个时刻的小波系数实部值。实部为正,意味着该时刻该周期尺度的信号相位与余弦波“对齐”,降雨量处于偏多状态;实部为负则相反,偏少。如果某个周期尺度上,图呈现明显的红色-蓝色交替带状结构,说明这个周期确实存在,且交替的快慢就是周期长度。
举一个典型的例子。模拟数据里包含了12个月的年度周期和36个月的准三年周期。实部图上你会看到在周期约12月的位置,出现一条连续红蓝交替的色带,贯穿整个时间轴,这就是稳定的年循环。而在周期约36月的位置,能看出另一条交替带,但颜色强度会随时间变化——比如1970年代强、1990年代弱——这恰恰是准三年周期“时强时弱”的直观证据。
看实部图时,我总会提醒自己三个原则。第一,先看大尺度上的颜色分布,避免被细节带偏。第二,关注色带是否连续,断断续续的色带说明周期不稳定。第三,影响锥之外的东西不要下结论,边角区域的能量受边界效应干扰大。
3.2 小波功率谱与显著性:别把背景噪声当信号
功率谱图(模方等值线图)展示的是能量分布,颜色亮代表能量大。如果说实部图回答“有没有这个周期”,功率谱图回答“这个周期强不强”。
功率谱图上有两个关键元素:颜色亮度和黑色等值线。黑色等值线的意义是95%置信边界,边界内(signif>1的区域)说明该处的功率显著超过红噪声背景,可以认为是真实信号;边界外的亮色区域,哪怕看起来再亮,也大概率是随机波动的产物,不能当作真实周期来引用。
在解读时,我建议把注意力放在黑色等值线围成的区域上。比如前面模拟数据的功率谱图,你会看到明显两块显著区域:一块在周期8-16月附近,跨越整个时间轴;另一块在周期24-48月区域,呈断续分布。第一块对应年循环,非常稳定;第二块对应准三年振荡,在早期较强,中后期减弱甚至消失。这样解读,结论就很有说服力。
初次做小波分析的人,很容易把功率谱图上所有亮色区域都当作显著信号,这是最大的误区。小波功率谱的背景噪声水平并不均匀,直接比较亮度是不可靠的,必须依赖显著性检验线。
3.3 小波方差图:定位全时段的“主导周期”
小波方差图其实是把功率谱图在时间方向上求平均,得到一条一维曲线。它回答一个更宏观的问题:整个观测时段内,哪个周期尺度的总平均能量最大?
读法非常简单:横轴周期(对数刻度),纵轴平均功率。曲线出现峰的位置就是主导周期。如果只有一个明显峰,说序列存在单一主导周期;如果有多个峰,则需要比较峰高并关注峰值对应的周期尺度是否在显著性检验中通过。
我经常把小波方差图当作论文摘要的“证据图”——它把复杂的二维图简化成一条曲线,方便读者直接抓住核心结论。比如某流域降水分析发现,小波方差谱在2.8年和10.5年各有一个峰,那么结论就可以写成“研究时段内降雨量存在2.8年和10.5年的主导周期”,简洁有力。
还应该提醒一句:小波方差图显示的是平均强度,如果一个周期只在部分时段强、其他时段弱,它的平均功率照样会有峰,但峰值高度会被摊薄。所以小波方差图应该和功率谱图结合着看,两者互相印证,结论才站得住。
4. 常见问题与排查技巧实录
4.1 问题速查表
下面这个表格是我做这个小波雨水分析项目时整理的常见问题,基本覆盖了绝大多数人会遇到的坑。
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 小波图边缘出现大片异常亮色 | 边界效应,COI之外的数据不可信 | 参考COI边界,只解读锥内区域;可延长序列或选择更合适的pad策略 |
| 功率谱图上显著性区域很少 | 序列自相关性弱、噪声大,或周期信号本身较弱 | 检查数据是否做过标准化距平,尝试更长序列,或提高采样频率 |
| MK检验结果Z值总是为0 | 序列长度过短,或S统计量为0 | 确认n是否≥10,检查数据是否经过差分或标准化导致单调性消失 |
| 输入数据有NaN导致报错 | 函数内未做缺失值处理 | 先用fillmissing补值,或把NaN过滤后再调用函数 |
| 小波周期纵轴刻度奇怪 | 绘图中log2(period)未还原 | 用yticklabels(2.^yticks)还原为实际周期 |
| 计算速度很慢 | 序列太长(如逐日数据),尺度数太多 | 增大dj(如从0.25改为0.5),或先对日数据聚合为月数据 |
| MK检验和Sen斜率方向相反 | 数据中存在极强的局部趋势反转 | 这是正常现象,需结合可视化确认分段趋势,报告中如实描述 |
| 小波系数实部图颜色跳跃剧烈 | 色标范围选择不当 | 设置clim,比如caxis([-max(abs(real(wave))), max(abs(real(wave)))]) |
表中每一条我都实际遇到过。特别是
