方向角有周期。179° 与 -179° 只相差 2°,算术平均却得到 0°,把一个朝后的方向算成了朝前。这个问题经常出现在航向、相位和旋转角的处理中。
本文给出一个仅依赖基础 MATLAB 的等权方向平均函数,并用原生 MATLAB R2025b 执行边界测试、1000 组确定种子的随机对称样本和故意错误实现。这里验证的是数值算法,不包含传感器、实机或上板实验。
1. 先定义返回值
输入 theta 是非空实数 double 向量,单位为度,范围为 [-180,180]。每个观测具有相同权重。输出 mu 统一到 [-180,180),所以 180° 表示为 -180°。
先把每个方向映射到单位圆,再平均两个分量:
C = mean(cosd(theta)),S = mean(sind(theta))。
R = hypot(C,S) 是平均合向量长度,理论范围为 [0,1]。方向通过 atan2d(S,C) 获得。R 接近 1 表示方向集中;R 接近 0 时,样本可能相互抵消,合向量方向对扰动很敏感。
例如 [0,180] 的单位向量相消。此时即使某个浮点实现给出了一个角度,这个角度也不应该作为稳定的平均方向。函数因此同时返回 reliable,并在 R<minR 时把 mu 设为 NaN。
默认 minR=1e-6 只是防止接近退化合向量的数值保护阈值,不是统计置信度,也不是推荐给所有业务的质量标准。业务可以选择更高门限,例如 0.8。门限相等时通过,R>=minR。
2. 完整实现
保存为 circular_mean_deg.m:
function [mu,R,reliable]=circular_mean_deg(theta,minR)
% Equal-weight directional mean; input degrees in [-180,180].
if nargin<2, minR=1e-6; end
validateattributes(theta,{'double'},{'real','vector','nonempty','finite','>=',-180,'<=',180});
validateattributes(minR,{'double'},{'real','scalar','finite','>',0,'<=',1});
c=mean(cosd(theta(:))); s=mean(sind(theta(:)));
R=min(1,hypot(c,s));
reliable=R>=minR;
if reliable
mu=mod(atan2d(s,c)+180,360)-180;
else
mu=NaN;
end
end
使用 cosd/sind 而不是把度直接交给 cos/sin。R 的上限截到 1,仅用于消除舍入造成的极小越界;它不把互相抵消的数据变成可靠数据。输入中的 NaN、Inf、矩阵及超范围角度都显式拒绝,避免悄悄忽略数据或把矩阵按列解释。
3. 可复现测试
下面测试采用手算边界和独立解析预期。随机样本围绕任意中心成对生成,每对偏移为 ±delta,且 delta<80°。每对的垂直分量相消、沿中心分量为正,因此真平均方向就是生成时的 center,不需要调用被测函数计算预期值;预期 R 为 mean(cosd(delta))。
保存为 test_circular.m:
function test_circular(f)
if nargin==0, f=@circular_mean_deg; end
[mu,r,ok]=f([179 -179]);
assert(abs(abs(mu)-180)<1e-12 && abs(r-cosd(1))<1e-14 && ok,'test:wrap','Cross-boundary mean failed');
[m,r,k]=f([0 180]); assert(isnan(m)&&r<1e-14&&~k);
[m,r,k]=f([90 90]); assert(m==90&&r==1&&k);
[m,~,~]=f(180); assert(m==-180);
[m,r,k]=f([0 120 -120]); assert(isnan(m)&&r<1e-14&&~k);
[~,~,k]=f([0 90],0.8); assert(~k);
[~,~,k]=f([0 90],0.7); assert(k);
invalid={[],NaN,Inf,[1 2;3 4],single(1),181,1i};
for j=1:numel(invalid), mustError(@()f(invalid{j})); end
for threshold={0,2,NaN,[],single(0.1)}, mustError(@()f(0,threshold{1})); end
rng(20261110,'twister'); worst=0;
% Analytic oracle: symmetric pairs within 80 degrees have mean center.
for j=1:1000
center=-180+360*rand; delta=80*rand(1,20);
a=mod(center+[-delta delta]+180,360)-180;
[m,r,k]=f(a); err=abs(mod(m-center+180,360)-180);
assert(err<1e-10 && k); assert(abs(r-mean(cosd(delta)))<1e-13);
worst=max(worst,err);
end
fprintf('CIRCULAR PASS: 7 deterministic, 12 invalid, 1000 symmetric random; max error=%.17g deg\n',worst);
fprintf('CROSS: mu=%.17g R=%.17g reliable=%d\n',mu,cosd(1),ok);
end
function mustError(fn)
caught=false; try, fn(); catch, caught=true; end; assert(caught,'test:reject','Expected rejection');
end
为证明测试能发现真实算法错误,保存下面故意错误的负控为 bad_circular.m。它不是推荐实现:
function [mu,R,reliable]=bad_circular(theta,varargin)
mu=mean(theta); R=1; reliable=true;
end
在这些文件所在目录打开 PowerShell,按本机安装位置修改 MATLAB 路径:
& 'D:\tools\matlab\R2025b\bin\matlab.exe' -batch "test_circular" -logfile circular_ok.log
$LASTEXITCODE
& 'D:\tools\matlab\R2025b\bin\matlab.exe' -batch "test_circular(@bad_circular)" -logfile circular_bad.log
$LASTEXITCODE
4. 原生运行结果
测试环境为 MATLAB 25.2.0.2998904 (R2025b),Windows。生产实现通过 7 个确定性场景、12 个非法输入场景、1000 组随机对称样本。每组随机样本含 40 个角度,固定 rng(20261110,‘twister’)。
随机样本相对解析中心的最大圆周角误差为 5.6843418860808015e-14°;测试允许误差为 1e-10°。跨边界输入 [179,-179] 的结果为 mu=-180°,R=0.99984769515639127,reliable=true。
[0,180] 和 [0,120,-120] 均触发低合向量保护,返回 NaN 和 reliable=false。[0,90] 的 R 约为 0.7071,因此门限 0.8 时拒绝,0.7 时通过。
正式实现的联合测试进程退出码为 0。故意使用算术平均的负控在运行期触发 test:wrap 断言,错误文本为 Cross-boundary mean failed,MATLAB 退出码为 1。该负控不是语法错误或缺少依赖造成的失败。
5. 使用边界
这个函数处理“有箭头”的方向数据。对于 0° 和 180° 代表同一条轴的无向轴数据,需要先明确轴向统计定义,不能直接复用本函数。
R 不是置信概率。单个样本的 R=1,也不能证明采样数量充分;两簇互相相反的真实方向可能让 R 接近零,这时一个平均值本来就不足以描述数据,应考虑分布图或分组结果。
输入范围限制避免了巨大角度在浮点表示和角度约简时的额外问题。若业务接收多圈累计角,需要在保留所需信息的前提下另行规范化。该实现未包含权重、缺失值剔除、置信区间和时间滤波,也没有声称对所有方向分布完成统计验证。



被折叠的 条评论
为什么被折叠?



