实测环境:MATLAB 25.2.0.2998904(R2025b),Windows 64 位(PCWIN64),2026-10-15。基础 MATLAB 即可。示例数据在代码中生成,不读取外部文件。保存为 interpolation_demo.m,切换到对应文件夹后运行:
matlab
results = interpolation_demo;
程序输出 results/02_aligned.csv、results/02_interpolation.png 和 results/02_interpolation.mat。本文采用已实测的修正版:17 个断言通过,日志输出 INTERPOLATION PASS;CSV 写出再读回后,8 个 UTC 时间戳保持唯一,最大时间误差为 0 秒。
两台设备都记录了六个数,并不意味着它们的第六个数发生在同一时刻。直接按行相减,可能把真实变化误当成设备偏差。
下面把两组不规则时间戳的数据对齐到一个公共网格,再用一个已知真值的例子说明:时间对齐之后,插值误差仍可能影响两台设备的差值。最后给出域外查询和长缺口的明确处理办法。
公共时间基准比插值方法更先确定
本文把两个设备的时间都表示成同一个起点之后的秒数:
matlab
t0 = datetime(2026,10,15,8,0,0,'TimeZone','UTC');
tA = [0 0.7 1.4 2.2 3.1 4.0]';
tB = [0.4 1.1 2.0 2.7 3.6 4.5]';
t0 是本次合成实验的人为起点。真实数据若已经有带时区的时间戳,可以选定一个共同起点,再分别用 seconds(timeA-t0) 和 seconds(timeB-t0) 转成数值秒。不要把 A 的第一条记录和 B 的第一条记录分别设为零,否则设备启动时差会被悄悄抹掉。
时间戳排序、时区统一与设备时钟校正是不同问题。插值不会估计时钟偏差,也不会自动修复时钟漂移;如果两个设备的时间基准本来不同,需要先处理同步问题。
在调用插值前,本例检查了长度一致、数值有限、时间严格递增。实际数据出现重复时间戳时,应先根据测量含义决定取均值、保留特定记录或报错。仅用 unique 丢掉记录,可能掩盖冲突值。
查询网格只覆盖共同有效区间
A 的时间范围是 [0,4] 秒,B 的时间范围是 [0.4,4.5] 秒。因此共同有效区间是 [0.4,4] 秒。
设网格间隔 dt=0.5 秒,并要求网格锚定同一个 t0。第一个合法网格编号是 ceil(0.4/0.5)=1,最后一个是 floor(4/0.5)=8,得到:
text
0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0
"公共区间的起点"不一定是"公共网格的第一个点"。明确网格锚点,才能让不同批次或不同设备组合生成可比较的时间轴。本例还设置断言,防止区间虽然相交、选定网格却没有任何点时继续对空结果运算。当前这组数据不会触发该断言。
随后对两个设备使用同一组查询坐标:
matlab
Aq = interp1(tA,yA,tq,'linear',NaN);
Bq = interp1(tB,yB,tq,'linear',NaN);
interp1 的采样坐标需要互不相同;linear 在邻近采样点间计算线性插值。这里显式把域外值设置为 NaN,让边界政策直接体现在代码中。接口细节见 R2025b interp1 文档。
真偏差恒为 3 为什么对齐结果会变化
定义已知真值:
g ( t ) = t 2 + 0.5 t + 2 g(t)=t^2+0.5t+2 g(t)=t2+0.5t+2
A 记录 g(tA),B 记录 g(tB)+3。两个设备在同一真实时刻的差值始终为 3,数据没有随机噪声。
但两组数据的邻近采样点不相同,各自的直线插值也不同。本次 MATLAB 实测对齐结果如下,为便于阅读保留两位小数:
| 相对时间 s | A 插值 | B 插值 | B 减 A | 相对真偏差 3 的误差 |
|---|---|---|---|---|
| 0.5 | 2.60 | 5.56 | 2.96 | -0.04 |
| 1.0 | 3.62 | 6.56 | 2.94 | -0.06 |
| 1.5 | 5.07 | 8.20 | 3.13 | 0.13 |
| 2.0 | 7.12 | 10.00 | 2.88 | -0.12 |
| 2.5 | 9.68 | 12.60 | 2.92 | -0.08 |
| 3.0 | 12.58 | 15.68 | 3.10 | 0.10 |
| 3.5 | 16.20 | 19.08 | 2.88 | -0.12 |
| 4.0 | 20.00 | 23.20 | 3.20 | 0.20 |
以 4 秒为例,A 正好有样本,所以 A 的插值等于真值 20。B 需要在 3.6 秒和 4.5 秒之间插值,结果是 23.2,因此差值变成 3.2。
这个误差可以直接推导。对二次函数 g(t),用区间端点 a,b 连接成直线,在区间内得到的误差为:
L ( t ) − g ( t ) = ( t − a ) ( b − t ) L(t)-g(t)=(t-a)(b-t) L(t)−g(t)=(t−a)(b−t)
4 秒处 B 的误差就是 (4-3.6)*(4.5-4)=0.2。它来自曲率与采样间隔,并不能被解释成设备偏差突然变化了 0.2。
在长度为 h 的区间内,本例的线性插值误差最大为 h^2/4。把查询网格从 0.5 秒改成 0.01 秒,只会在同一条分段直线上生成更多点,不会缩短原始采样缺口,也不会降低这条直线相对真值的固有误差。
外推必须单独决定
程序再查询 A 数据范围之外的 -0.5 秒和 4.5 秒:
- 使用
'linear',NaN:两个结果都是NaN - 使用
'linear','extrap':分别得到 1.4 和 23.8 - 已知合成真值分别为 2.0 和 24.5,所以外推误差分别为 -0.6 和 -0.7
'extrap' 只是允许把端点附近的方法继续用于范围之外,并不保证趋势仍然成立。这里的二次曲率已经足以让线性外推偏离真值。
不要把"interp1 默认不外推"当作适用于所有方法的规则。R2025b 文档说明 pchip、spline、makima 的默认域外行为与 linear 不同。需要保持统一边界策略时,显式提供最后一个参数更清楚。
区间内部的长缺口也可能不应填补
只保留公共区间,并不能避免跨越很长的停采段。程序另设样本时间 [0,1,4],把允许桥接的最大间隔定为 1 秒。查询 2 秒、3 秒虽然位于整体数据范围内,却跨越了 1 秒到 4 秒的三秒缺口,因此保留为 NaN。
做法是找到每个查询点左、右两侧对应的样本时刻,比较间隔:
matlab
left = interp1(tGap,tGap,qGap,'previous',NaN);
right = interp1(tGap,tGap,qGap,'next',NaN);
bridgesLongGap = (right-left)>1;
zGap(bridgesLongGap) = NaN;
恰好落在原始样本上的查询点,两侧坐标相同,仍保留原始观测。这里的 1 秒只是实验规则;真实阈值应根据对象变化速度、采样约定和允许误差确定。图中的红叉只标出被拒绝的位置,纵坐标借用已知合成真值帮助阅读,并不是填补后的有效数据。
CSV 导出也要验证时间精度
初版代码在内存中生成了正确的半秒网格,直接把默认格式的 datetime 写入 CSV 后,却只保留到了整秒。实际导出的 8 行数据只有 5 个不同时间戳,重新解析后的最大时间误差达到 0.5 秒。例如,原本的 1 秒与 1.5 秒都写成了 15-Oct-2026 08:00:01。
数值插值通过断言,并不表示导出的时间轴也正确。修正版先把时间明确转换到 UTC,再指定毫秒格式:
matlab
timestampFormat = 'yyyy-MM-dd''T''HH:mm:ss.SSS''Z''';
aligned.Timestamp.TimeZone = 'UTC';
aligned.Timestamp.Format = timestampFormat;
这里格式中的 Z 是字面标记,真正的时区转换由 TimeZone='UTC' 完成,不能给未经转换的本地时间直接加上 Z。修正版首行时间为 2026-10-15T08:00:00.500Z,末行为 2026-10-15T08:00:04.000Z。
完整代码写出 CSV 后,把时间列按字符串读回,再按指定格式解析,检查 UTC 标记、时间戳唯一性、实际时间网格和相对秒数。实测 8 个时间戳全部唯一,最大回读时间误差为 0 秒。SSS 的毫秒精度足以表示本例的半秒网格;如果你的数据含有更细的时间分辨率,应提高格式精度并保留同样的往返检查。格式与写出接口分别见 R2025b datetime 和 R2025b writetable。
运行后检查输出
本次实测公共网格有 8 个点,对齐差值相对真偏差的最大绝对误差显示为 0.2;域外结果为 NaN;显式外推结果为 [1.4,23.8];长缺口规则拒绝 2 个查询点。数值检查与 CSV 回读检查全部通过。

CSV 保留公共时间戳、相对秒数、A、B、差值和已知真值下的误差。图 02_interpolation.png 包含原始样本与公共网格、对齐后差值、外推误差和长缺口政策四个面板。
实际测量通常没有已知真值,CSV 中最后一列便不能直接计算。此时应保留质量标记、插值方法、邻近样本间隔和域外状态,避免下游分析把所有网格点都视为同等可靠。对于状态码、开关量和类别值,也应重新选择适当的时间对齐方法,不能照搬连续量的线性插值。
完整可运行代码
保存为 interpolation_demo.m,文件名必须与第一行函数名一致。
matlab
function results = interpolation_demo(outDir)
% Common-time-grid interpolation and explicit extrapolation policy.
% Base MATLAB, R2025b target. Run: results = interpolation_demo;
if nargin < 1
outDir = fullfile(fileparts(mfilename('fullpath')), 'results');
end
if ~exist(outDir, 'dir'), mkdir(outDir); end
% All times are seconds relative to the SAME origin.
t0 = datetime(2026,10,15,8,0,0,'TimeZone','UTC');
tA = [0 0.7 1.4 2.2 3.1 4.0]';
tB = [0.4 1.1 2.0 2.7 3.6 4.5]';
g = @(t) t.^2 + 0.5*t + 2;
yA = g(tA);
yB = g(tB) + 3;
assert(all(isfinite([tA;yA;tB;yB])), 'Nonfinite input.');
assert(all(diff(tA)>0) && all(diff(tB)>0), 'Times must increase strictly.');
assert(numel(tA)==numel(yA) && numel(tB)==numel(yB), 'Length mismatch.');
dt = 0.5;
lo = max(tA(1),tB(1)); hi = min(tA(end),tB(end));
assert(lo <= hi, 'The records do not overlap.');
% Anchor the common grid to t0, and keep only the overlap.
kFirst = ceil(lo/dt); kLast = floor(hi/dt);
assert(kFirst <= kLast, 'No common grid point at this dt.');
tq = (kFirst:kLast)'*dt;
Aq = interp1(tA,yA,tq,'linear',NaN);
Bq = interp1(tB,yB,tq,'linear',NaN);
assert(all(isfinite([Aq;Bq])), 'Unexpected out-of-range interpolation.');
expectedA = [2.60 3.62 5.07 7.12 9.68 12.58 16.20 20.00]';
expectedB = [5.56 6.56 8.20 10.00 12.60 15.68 19.08 23.20]';
assert(max(abs(Aq-expectedA)) < 1e-11, 'A interpolation mismatch.');
assert(max(abs(Bq-expectedB)) < 1e-11, 'B interpolation mismatch.');
% The true offset is 3, but two different interpolation errors remain.
offsetError = (Bq-Aq)-3;
assert(abs(max(abs(offsetError))-0.20) < 1e-11, 'Offset-error mismatch.');
% Domain boundary and extrapolation are separate, explicit experiments.
tOutside = [-0.5; 4.5];
noExtrap = interp1(tA,yA,tOutside,'linear',NaN);
withExtrap = interp1(tA,yA,tOutside,'linear','extrap');
assert(all(isnan(noExtrap)), 'Outside queries must be NaN.');
assert(max(abs(withExtrap-[1.4;23.8])) < 1e-11, 'Extrapolation mismatch.');
extrapError = withExtrap-g(tOutside);
assert(max(abs(extrapError-[-0.6;-0.7])) < 1e-11, 'Extrapolation error mismatch.');
% A minimal long-gap policy: preserve samples, reject bridged gaps > 1 s.
tGap = [0;1;4]; yGap = g(tGap); qGap = [0;0.5;1;2;3;4];
zGap = interp1(tGap,yGap,qGap,'linear',NaN);
left = interp1(tGap,tGap,qGap,'previous',NaN);
right = interp1(tGap,tGap,qGap,'next',NaN);
bridgesLongGap = (right-left)>1;
zGap(bridgesLongGap) = NaN;
assert(isequal(isnan(zGap),[false;false;false;true;true;false]), ...
'Long-gap policy failed.');
aligned = table(t0+seconds(tq),tq,Aq,Bq,Bq-Aq,offsetError, ...
'VariableNames',{'Timestamp','TimeSeconds','A','B','BminusA','OffsetError'});
% v2: preserve fractional seconds and explicitly identify UTC in the CSV.
timestampFormat = 'yyyy-MM-dd''T''HH:mm:ss.SSS''Z''';
aligned.Timestamp.TimeZone = 'UTC';
aligned.Timestamp.Format = timestampFormat;
csvPath = fullfile(outDir,'02_aligned.csv');
writetable(aligned,csvPath);
opts = detectImportOptions(csvPath);
opts = setvartype(opts,'Timestamp','string');
roundTrip = readtable(csvPath,opts);
assert(all(endsWith(roundTrip.Timestamp,"Z")), ...
'interpolation:CSVZone','CSV timestamps must explicitly identify UTC.');
parsedTime = datetime(roundTrip.Timestamp,'InputFormat',timestampFormat,'TimeZone','UTC');
assert(numel(parsedTime)==numel(tq) && numel(unique(parsedTime))==numel(tq), ...
'interpolation:CSVUnique','CSV timestamps must remain distinct.');
csvRoundTripMaxError = max(abs(seconds(parsedTime-t0)-tq));
assert(csvRoundTripMaxError < 1e-9, ...
'interpolation:CSVGrid','CSV timestamps must preserve the intended time grid.');
assert(max(abs(roundTrip.TimeSeconds-tq)) < 1e-9, ...
'interpolation:CSVSeconds','CSV relative seconds must preserve the intended grid.');
fprintf('INTERPOLATION CSV roundtrip_points = %d; max_time_error_seconds = %.16g\n', ...
numel(unique(parsedTime)),csvRoundTripMaxError);
tFine = linspace(-0.5,4.5,501)';
fig = figure('Color','w','Position',[100 100 1100 750]);
tiledlayout(2,2,'TileSpacing','compact','Padding','compact');
nexttile;
plot(tA,yA,'o',tB,yB,'s',tq,Aq,'-',tq,Bq,'-','LineWidth',1.2);
xlabel('Seconds from common origin'); ylabel('Value'); grid on;
title('Different timestamps, one query grid');
legend('A samples','B samples','A linear','B linear','Location','northwest');
nexttile;
plot(tq,Bq-Aq,'o-','LineWidth',1.2); hold on; yline(3,'k--');
xlabel('Seconds from common origin'); ylabel('B - A'); grid on;
title('True offset = 3; interpolation errors differ');
nexttile;
plot(tFine,g(tFine),'k-',tA,yA,'o',tOutside,withExtrap,'rx','LineWidth',1.2);
xline(tA(1),'--'); xline(tA(end),'--');
xlabel('Seconds from common origin'); ylabel('Value'); grid on;
title('Endpoint linear extrapolation misses curvature');
legend('Known synthetic truth','Samples','Extrapolated','Location','northwest');
nexttile;
plot(tGap,yGap,'ko',qGap,zGap,'b.-','LineWidth',1.2); hold on;
plot(qGap(bridgesLongGap),g(qGap(bridgesLongGap)),'rx','MarkerSize',9);
xlabel('Time (s)'); ylabel('Value'); grid on;
title('Reject interpolation across gaps > 1 s');
legend('Samples','Accepted interpolation','Rejected query locations', ...
'Location','northwest');
exportgraphics(fig,fullfile(outDir,'02_interpolation.png'),'Resolution',160);
results = struct('querySeconds',tq,'A',Aq,'B',Bq, ...
'maxOffsetError',max(abs(offsetError)), ...
'outsideQuery',tOutside,'extrapolated',withExtrap, ...
'extrapolationError',extrapError,'longGapRejected',nnz(bridgesLongGap));
disp(aligned);
fprintf('INTERPOLATION common_points = %d; max_offset_error = %.12g\n', ...
numel(tq),results.maxOffsetError);
fprintf('INTERPOLATION extrapolated = [%.12g %.12g]; errors = [%.12g %.12g]\n', ...
withExtrap,extrapError);
fprintf('INTERPOLATION long_gap_rejected = %d\n',results.longGapRejected);
fprintf('INTERPOLATION PASS\n');
save(fullfile(outDir,'02_interpolation.mat'),'results');
end
参考资料
- MathWorks R2025b interp1
- MathWorks R2025b datetime
- MathWorks R2025b writetable
- MathWorks R2025b exportgraphics
文中的时间戳、数据、误差分析和实验代码均为本文构造。