实测说明(2026-10-17):本文代码已在 Windows 11(构建 26200)、MATLAB 25.2.0.2998904(R2025b)、PCWIN64 中实际执行,主实验及独立边界补测均通过。表格和配图来自真实 MATLAB 输出,补测用 QR 求解与双边残差化交叉核对。requiredFilesAndProducts 实际分析仅返回基础 MATLAB,不依赖 Curve Fitting Toolbox、Statistics and Machine Learning Toolbox 或 Signal Processing Toolbox。先期 Python 参考模型结果另行保留,不混作 MATLAB 实测数据。
最短运行:将后文完整代码保存为 detrend_joint_fit_demo.m,在文件所在目录执行 results = detrend_joint_fit_demo;。真实关键结果:先去线性趋势再拟合的幅值为 0.686491473;联合拟合幅值为 0.8。
测量一个已知频率的低频正弦,数据中还有直流偏置与慢漂移。一个很自然的流程是先 detrend,再对剩余信号测幅相。
本例中,真实幅值为 0.8,频率为 0.25 Hz。先去线性趋势再拟合正弦,得到的幅值约为 0.68649,低了 14.19%;初相位也从 0.6 rad 变成约 0.26571 rad。这个偏差在无噪声输入上就存在。
原因在于:短记录里的正弦分量,与常量和线性趋势通常不正交。去趋势时,真实信号的一部分也被分给了"漂移"。
1. 先明确已知量和待估计量
适用场景是注入测试、锁定参考频率的传感器测量、已知激励频率的系统响应分析。频率 f0 来自可信参考,待估计的是幅值、相位、偏置和漂移斜率。
输入模型为:
x ( t ) = A cos ( 2 π f 0 t + ϕ ) + c + d ( t − t ˉ ) x(t)=A\cos(2\pi f_0t+\phi)+c+d(t-\bar t) x(t)=Acos(2πf0t+ϕ)+c+d(t−tˉ)
用三角恒等式展开:
x ( t ) = a cos ( 2 π f 0 t ) + b sin ( 2 π f 0 t ) + c + q τ x(t)=a\cos(2\pi f_0t)+b\sin(2\pi f_0t)+c+q\tau x(t)=acos(2πf0t)+bsin(2πf0t)+c+qτ
其中 tau=(t-mean(t))/timeScale,timeScale=max(abs(t-mean(t))),所以 q=d*timeScale。
系数关系为:
a = A cos ϕ , b = − A sin ϕ a=A\cos\phi,\quad b=-A\sin\phi a=Acosϕ,b=−Asinϕ
于是:
A = a 2 + b 2 , ϕ = atan2 ( − b , a ) A=\sqrt{a^2+b^2},\qquad\phi=\operatorname{atan2}(-b,a) A=a2+b2 ,ϕ=atan2(−b,a)
这里使用余弦相位约定。若原始定义采用正弦,转换公式也要一起修改,不能只把图标题改成"正弦相位"。atan2 接收的参数顺序是纵坐标、横坐标,返回四象限角度。MathWorks atan2 文档
2. 只对数据去趋势,会改变后续模型
定义正弦基矩阵 S=[cos(2*pi*f0*t), sin(2*pi*f0*t)],趋势基矩阵 Z=[ones(N,1), tau]。
线性去趋势可以写成:
matlab
yDetrend = x - Z*(Z\x);
betaSequential = S\yDetrend;
第一行把 x 在常量和直线方向上的最小二乘投影去掉;第二行却仍然用原来的正弦、余弦基去解释已经变形的信号。这两步一般不等于联合估计。
MathWorks 将 detrend 描述为去除最小二乘意义下的最佳拟合直线,detrend(x,0) 则去均值。是否应去掉趋势要由分析目标决定。MathWorks detrend 文档
本例采用 Fs=100 Hz、N=400、f0=0.25 Hz,标称记录时长 N/Fs=4 s,对应一个周期的离散样本;最后一个样本时刻是 3.99 s。正弦与常量项在这组采样上可以正交,但它与线性时间项仍会相关。因此,"已经采了整周期"不能作为任意去线性趋势后幅相不变的保证。
偏差还取决于起始相位、频率和记录长度。本文的 14.19% 是指定数据的结果,不是 detrend 的固定衰减系数,更不是可直接补偿的滤波器增益。
3. 把目标分量与漂移放进同一个最小二乘问题
一次性建立四列矩阵:
matlab
X = [S, Z];
betaJoint = X\x;
使用四个系数同时解释原始数据。在频率正确、模型匹配、矩阵满列秩且数值条件合理的前提下,无噪声数据可在浮点精度范围内恢复参数。
MATLAB 的反斜杠运算可以对长方形矩阵求最小二乘解。MathWorks mldivide 文档
工程代码不必显式计算 inv(X'*X)*X'*x。对满列秩矩阵,形成法方程会把 2 范数条件数平方,还增加一次显式求逆。直接使用 X\x,保留求解器选择稳定路径的空间。
时间列先居中再缩放到约 [-1,1],可以避免直接放入很大的绝对时间戳。但这只能改善列尺度,不能解决"只观察到极小一段正弦时,它本身就很像常量加直线"的可辨识性问题。
4. 已经去趋势的数据,模型基也要做同样处理
如果处理流程必须先做去趋势,可以对正弦基也施加相同操作:
matlab
SResidual = S - Z*(Z\S);
betaPartial = SResidual\yDetrend;
也就是同时对数据和模型去除同一个趋势子空间。这个结果与联合拟合的前两个系数相同,适用条件是参与计算的矩阵满足所需的秩条件。
从分块最小二乘可以直接推导:对任意正弦系数,最佳趋势系数会消去趋势方向的残差,余下的优化只需要比较残差化之后的 S 与 x。因此这里是一项有独立数学依据的交叉检查。
代码没有构造 N×N 的投影矩阵,避免为了一个四参数问题浪费二次方规模的内存。MATLAB 独立补测在 6 种周期数、4 种初相位共 24 组配置下,用 QR 分解单独求解,并与反斜杠、已知参数和双边残差化结果交叉核对;三项最大差分别为约 1.68×10⁻¹⁴、9.44×10⁻¹⁵ 和 8.99×10⁻¹⁵。先期 Python 参考检查单独保留。
5. 本次 MATLAB 实测结果
| 检查项 | 本次 MATLAB 结果 |
|---|---|
| 真实幅值 / 相位 | 0.8 / 0.6 rad |
| 仅数据去趋势后拟合 | 0.686491473 / 0.265707073 rad |
| 顺序处理的幅值相对误差 | -14.1886% |
| 联合拟合幅值 / 相位 | 约 0.8 / 0.6 rad |
| 联合拟合直流 / 斜率 | 约 1.7 / 0.12 输入单位每秒 |
| 联合模型条件数 | 3.37292 |
| 双边残差化与联合系数最大差值 | 1.11×10⁻¹⁶ |
| 显式投影与内置 detrend 最大差值 | 4.44×10⁻¹⁶ |
| 无噪声模型最大残差检查 | 小于 1×10⁻¹²,断言通过 |
加入幅度为 0.01 的 17 Hz 正弦和幅度为 0.006 的 23 Hz 余弦后,估计幅值为 0.800155757,相位为 0.600289319 rad,残差 RMS 为 0.008244689。它们是可重复的确定性干扰,不能把这一次结果解释为白噪声统计实验或置信区间。
把拟合频率故意提高 5%,即使用 0.2625 Hz,即便原始输入没有噪声,幅值也变为约 0.768046944,残差 RMS 为 0.018586864。频率是这个方法的前提,联合拟合不会自动修复错误的频率参数。

上图为本次 MATLAB 实际导出。左下图在固定相位 0.6 rad 下改变记录内的标称周期数;它展示本组数据的顺序处理偏差,不应当作普遍的幅频响应。右下图展示联合模型也有自己的短窗口限制。
6. 只采到 0.05 个周期,联合拟合也会变得脆弱
把记录内的标称周期数 f*N/Fs 从 1 减小到 0.05,条件数从约 3.37 增加到约 14647.38。此时,正弦的局部形状与低阶趋势很接近,小的干扰和建模误差都可能造成较大的参数变化。
cond(X) 的默认 2 范数条件数由最大、最小奇异值的比值得到,可用来观察这种敏感性。MathWorks cond 文档
独立 MATLAB 补测还向两组模型各自最小奇异值对应的左奇异向量方向加入相对范数为 1e-6 的确定性输入扰动。0.05 周期时,系数向量变化的范数约为 0.02367,估计幅值从约 0.8 变为 0.816235;1 周期时,系数向量变化范数约为 6.12×10⁻⁶,幅值约为 0.799998。这是专门构造的敏感性反例,不是随机噪声统计结论。
不存在一个对所有测量系统都合适的固定条件数门限。应结合允许的幅相误差、信噪比、参考频率稳定度和模型残差评估。条件数较小也不代表模型一定正确。
若 f0=0,余弦列与直流列重复,正弦列全零,四个参数无法独立识别。本例还显式检查了这个降秩边界。均匀实采样在奈奎斯特频率处同样不能按普通双正交基方式任意估计幅相。实际应用应避开这些退化点。
7. 完整 MATLAB 示例与运行方式
保存为 detrend_joint_fit_demo.m,在文件目录执行:
matlab
results = detrend_joint_fit_demo;
函数包含顺序处理反例、联合拟合、残差化对照、错误频率对照、短窗扫描和结果导出。本次实际日志已打印 JOINT MATLAB PASS,11 处断言各执行一次,主程序批次进程退出码为 0。与同批 FIR 实验合计,主程序有 20 次成功断言执行,独立补测有 103 个通过检查;另有两次 MAT 保存回读核验,不混入前述计数。运行前用 version 和 ver 保存实际环境;图和 MAT 文件输出到 matlab_results,建议每次使用新的输出目录。
matlab
function results = detrend_joint_fit_demo(outDir)
% Base-MATLAB joint sinusoid and linear-trend least-squares demo.
% Compares sequential detrending, joint fitting, and conditioning limits.
if nargin < 1
outDir = fullfile(fileparts(mfilename('fullpath')), 'matlab_results');
end
if ~exist(outDir,'dir'), mkdir(outDir); end
Fs = 100; N = 400; f0 = 0.25;
A = 0.8; phi = 0.6; dc = 1.7; slopePerSecond = 0.12;
t = (0:N-1)'/Fs;
tc = t-mean(t);
timeScale = max(abs(tc));
tau = tc/timeScale;
S = [cos(2*pi*f0*t), sin(2*pi*f0*t)];
Z = [ones(N,1), tau];
X = [S, Z];
signal = A*cos(2*pi*f0*t+phi);
trend = dc+slopePerSecond*tc;
x = signal+trend;
% Wrong comparison: remove the best-fit line from y but leave S unchanged.
yDetrend = x-Z*(Z\x);
betaSequential = S\yDetrend;
aSequential = hypot(betaSequential(1),betaSequential(2));
pSequential = atan2(-betaSequential(2),betaSequential(1));
% Joint model: cosine, sine, offset, and centered/scaled time together.
betaJoint = X\x;
aJoint = hypot(betaJoint(1),betaJoint(2));
pJoint = atan2(-betaJoint(2),betaJoint(1));
yJointSignal = S*betaJoint(1:2);
residual = x-X*betaJoint;
% Independent algebraic check: remove Z from BOTH y and signal columns.
SResidual = S-Z*(Z\S);
betaPartial = SResidual\yDetrend;
partialError = max(abs(betaPartial-betaJoint(1:2)));
detrendError = max(abs(detrend(x)-yDetrend));
phaseError = atan2(sin(pJoint-phi),cos(pJoint-phi));
assert(abs(aJoint-A)<1e-12, 'JOINT:Amplitude','Joint amplitude mismatch.');
assert(abs(phaseError)<1e-12, 'JOINT:Phase','Joint phase mismatch.');
assert(abs(betaJoint(3)-dc)<1e-12, 'JOINT:DC','DC mismatch.');
assert(abs(betaJoint(4)/timeScale-slopePerSecond)<1e-12, ...
'JOINT:Slope','Slope mismatch.');
assert(partialError<1e-12, 'JOINT:Partial','Residualized model differs.');
assert(detrendError<1e-12, 'JOINT:Detrend','Explicit detrend differs.');
assert(abs(aSequential/A-1)>0.1, 'JOINT:NegativeControl','Bias not exposed.');
assert(max(abs(residual))<1e-12, 'JOINT:Residual','Exact model mismatch.');
% Reproducible extra tones, not random white noise or an uncertainty model.
interference = 0.01*sin(2*pi*17*t)+0.006*cos(2*pi*23*t);
xDisturbed = x+interference;
betaDisturbed = X\xDisturbed;
aDisturbed = hypot(betaDisturbed(1),betaDisturbed(2));
pDisturbed = atan2(-betaDisturbed(2),betaDisturbed(1));
rmsDisturbed = sqrt(mean((xDisturbed-X*betaDisturbed).^2));
% Frequency is an INPUT to this method. A wrong frequency changes the result.
fWrong = 1.05*f0;
XWrong = [cos(2*pi*fWrong*t),sin(2*pi*fWrong*t),Z];
betaWrong = XWrong\x;
aWrong = hypot(betaWrong(1),betaWrong(2));
rmsWrong = sqrt(mean((x-XWrong*betaWrong).^2));
assert(rmsWrong>1e-3, 'JOINT:WrongFrequency','Frequency mismatch not exposed.');
% Number of nominal cycles = f*N/Fs. Short windows approach a line.
cycles = [0.05 0.1 0.25 0.5 1 2 4 10]';
conditionNumbers = zeros(size(cycles));
sequentialRatios = zeros(size(cycles));
for k = 1:numel(cycles)
f = cycles(k)/(N/Fs);
Sk = [cos(2*pi*f*t),sin(2*pi*f*t)];
Xk = [Sk,Z];
xk = A*cos(2*pi*f*t+phi)+trend;
ck = Sk\(xk-Z*(Z\xk));
conditionNumbers(k) = cond(Xk);
sequentialRatios(k) = hypot(ck(1),ck(2))/A;
end
assert(conditionNumbers(1)>1e4, 'JOINT:Condition','Short-window risk missing.');
assert(rank([ones(N,1),zeros(N,1),Z])<4, ...
'JOINT:ZeroFrequency','DC must be rank deficient.');
fig = figure('Visible','off','Color','w','Position',[100 100 1050 760]);
subplot(2,2,1);
plot(t,x,'k-',t,trend,'b--'); grid on;
xlabel('Time (s)'); ylabel('Input units'); legend('Tone + offset + drift','Known drift');
title('0.25 Hz tone in a 4 s nominal record');
subplot(2,2,2);
plot(t,signal,'k-',t,yDetrend,'r-',t,yJointSignal,'b--'); grid on;
xlabel('Time (s)'); ylabel('Input units');
legend('True tone','After linear detrend','Joint fitted tone');
title('Detrending also removes part of the tone');
subplot(2,2,3);
semilogx(cycles,sequentialRatios,'o-'); hold on;
plot([cycles(1),cycles(end)],[1,1],'k--'); grid on;
xlabel('Nominal cycles f N / Fs'); ylabel('Sequential amplitude / true amplitude');
title('Bias depends on window and phase; phi = 0.6 rad');
subplot(2,2,4);
loglog(cycles,conditionNumbers,'o-'); grid on;
xlabel('Nominal cycles f N / Fs'); ylabel('cond([cos sin 1 tau])');
title('Joint fitting also has a short-window limit');
print(fig,fullfile(outDir,'detrend_joint_fit_matlab.png'),'-dpng','-r150');
close(fig);
results = struct('Fs',Fs,'N',N,'f0',f0,'trueAmplitude',A,'truePhase',phi, ...
'sequentialAmplitude',aSequential,'sequentialPhase',pSequential, ...
'jointAmplitude',aJoint,'jointPhase',pJoint,'jointDC',betaJoint(3), ...
'jointSlope',betaJoint(4)/timeScale,'jointCondition',cond(X), ...
'partialError',partialError,'detrendError',detrendError, ...
'disturbedAmplitude',aDisturbed,'disturbedPhase',pDisturbed, ...
'disturbedResidualRMS',rmsDisturbed,'wrongFrequency',fWrong, ...
'wrongAmplitude',aWrong,'wrongResidualRMS',rmsWrong, ...
'cycles',cycles,'conditionNumbers',conditionNumbers, ...
'sequentialRatios',sequentialRatios);
save(fullfile(outDir,'detrend_joint_fit_matlab.mat'),'results');
fprintf('JOINT sequential_A=%.12g; sequential_phi=%.12g\n',aSequential,pSequential);
fprintf('JOINT A=%.12g; phi=%.12g; dc=%.12g; slope=%.12g; cond=%.12g\n', ...
aJoint,pJoint,betaJoint(3),betaJoint(4)/timeScale,cond(X));
fprintf('JOINT partial_error=%.16g; detrend_error=%.16g\n',partialError,detrendError);
fprintf('JOINT disturbed_A=%.12g; disturbed_phi=%.12g; residual_rms=%.12g\n', ...
aDisturbed,pDisturbed,rmsDisturbed);
fprintf('JOINT wrong_f=%.12g; wrong_A=%.12g; residual_rms=%.12g\n', ...
fWrong,aWrong,rmsWrong);
fprintf('JOINT MATLAB PASS; low_cycle_condition=%.12g\n',conditionNumbers(1));
end
8. 接入测量脚本时的检查顺序
- 从可信的激励源或采样参考得到
f0。若频率未知或随时间变化,应另做频率估计与模型选择;本文不覆盖这一阶段。 - 用真实采样时间建立正弦基和趋势基。对不等间隔时间戳,显式矩阵仍可建立,但默认按样本序号处理的
detrend(x)不再等于按真实时间去趋势,不能照搬该对照。 - 对缺失值做一致的行筛选,同时更新
x、t及所有基函数。不要只删数据、不删矩阵中的对应行。 - 检查矩阵秩、条件数、残差,以及观测窗内的周期数。增大窗口常常比增加拟合阶数更有用,但要确认较长窗口内频率和漂移模型仍成立。
- 把相位参考时刻写进结果。本例相位相对于
t=0;若平移时间原点,相位也要相应换算。幅值接近噪声水平时,即使atan2返回数字,相位也可能没有实际可信度。 - 保留原始数据和拟合出的漂移。真实慢变化可能就是要测的物理量,不能因为"看着不平"便默认删掉。高阶去趋势会进一步侵占低频子空间,必须重新验证目标信号误差。
这个例子的重点,是让预处理与后续模型保持一致。在有限观测窗口内,先删去一部分数据方向,再用未经修改的模型解释它,会造成可量化的系统偏差。已知频率条件下,把目标分量和干扰项放进同一个最小二乘问题,能把这种偏差显式地暴露并控制起来。
延伸阅读:MATLAB Hann 相干增益与补零标度对照。如果后续再用 FFT 检查拟合残差,仍需要独立处理窗口、幅值标度及非相干采样问题。
工程问题:如果你的观测窗只能覆盖 0.05 个周期,而频率参考又存在小幅误差,你会先延长窗口,还是仍直接报告联合拟合的幅值?可以沿用本文的条件数、频率误设和扰动测试,先检查结果对这些变化是否稳定。