把离散样本按位置分进若干区间,看起来只是调用一次 histcounts。但实际项目里,经常同时混着三种量:每箱有几个点、每箱累计了多少贡献、每个点的平均贡献是多少。若没有先写清端点规则和无效数据策略,落在边界上的记录可能被算两次,最右端样本可能漏掉,空箱也可能被错误解释为真实零值。
本文实现 bin_totals(x,w,edges):x 是每个样本的位置,w 是这个样本的一次贡献。函数同时返回计数 count、贡献和 total、样本平均贡献 meanValue,以及逐点箱号 bin。它处理的是离散样本的聚合,不是连续曲线的数值积分,不会自动乘以箱宽。
1. 先规定箱子的边界
若 edges=[0 1 2 3],三个箱子分别是 0,1)、\[1,2)、\[2,3。内部边界归右箱,最后一个箱子包含最右端点。这与 MATLAB histcounts 在显式边界下的端点约定一致。
因此 x=1 属于第二箱,x=2 和 x=3 都属于第三箱;x<0 或 x>3 则不属于任何箱,输出 bin=0,不参与计数和累计。每个点最多落入一个箱。不要对所有区间都同时写左闭右闭,否则内部边界会重复统计;也不要把最后一箱仍当作右开区间,否则终止位置恰好等于 edges(end) 的样本会丢失。
本文要求 edges 是至少含两个元素的有限、严格递增 double 向量。不允许重复边界,也不把 Inf 当开放端点。x 和 w 必须是等长、有限、实数、非稀疏 double 向量,行列方向可以不同;bin 保留 x 的形状,其余三个结果统一返回行向量。
无效数据策略采用"拒绝整次调用":x 或 w 含 NaN、Inf 时直接报错,而不是静默过滤。即使某个位置越界,它对应的非法 w 也不能绕过校验。如果业务要忽略无效记录,应在调用前显式生成掩码,并记录删去了多少条,不能把数据清洗隐藏在聚合函数内部。
2. 三个统计量不要混用
对第 j 个箱,count 是其中的样本数;total 是这些样本的 w 之和,也可以理解为以 w 为权重的直方图累计。w 全部为 1 时,total 才与 count 相同。w 可以为零或负数;零贡献的样本仍然是一个样本,必须计入 count。
meanValue 定义为 total/count,是"每个样本的平均贡献",不是 x 的加权平均。后者需要计算 sum(w.*x)/sum(w),还要另外处理分母为零和负权重的含义。这里也不计算概率密度,既不除以总样本数,也不除以箱宽。
空箱满足 count=0、total=0,但 meanValue=NaN。已有样本且贡献均为零的箱子,平均值则为 0。这两个状态含义不同,绘图、缺测检测或下游汇总时不应互相替代。
3. 完整函数:bin_totals.m
histcounts 负责统一生成箱号;accumarray 按非零箱号累计贡献。分箱和求和共享同一套箱号,不各自重写一套容易出现差异的边界判断。
matlab
function [count,total,meanValue,bin] = bin_totals(x,w,edges)
%BIN_TOTALS Bin locations x and sum their per-sample contributions w.
% All bins are [left,right), except the final bin is [left,right].
% Out-of-range points have bin=0 and contribute nothing.
valid = @(v) isa(v,'double') && isreal(v) && ~issparse(v) ...
&& (isvector(v) || isequal(size(v),[0 0])) && all(isfinite(v(:)));
if ~valid(x) || ~valid(w)
error('bins:Data','x and w must be finite real full double vectors.');
end
if numel(x)~=numel(w)
error('bins:Size','x and w must have the same number of elements.');
end
if ~valid(edges) || numel(edges)<2 || any(diff(edges(:))<=0)
error('bins:Edges','edges must be finite and strictly increasing.');
end
[count,~,b]=histcounts(x(:),edges(:).');
k=numel(edges)-1;
inside=b>0; values=w(:);
total=accumarray(b(inside),values(inside),[k 1],@sum,0).';
if any(~isfinite(total))
error('bins:Overflow','A bin total is not finite; rescale the contributions.');
end
meanValue=NaN(1,k);
occupied=count>0;
meanValue(occupied)=total(occupied)./count(occupied);
bin=reshape(b,size(x));
end
手算例子:
matlab
[count,total,meanValue,bin] = bin_totals([0 3],[0 8],[0 1 2 3]);
% count = [1 0 1]
% total = [0 0 8]
% meanValue = [0 NaN 8]
% bin = [1 3]
虽然输入都有限,累计和仍可能溢出,例如同一箱内相加两个 realmax。函数检查累计结果,一旦出现非有限值就抛出 bins:Overflow,不会把 Inf 当正常统计结果返回。这是一项失败检测,不是任意精度求和;若数值尺度不合适,应由调用方先缩放贡献并管理单位。
4. 可复现测试:test_bins.m
测试包含端点、边界相邻浮点数、空箱、空输入、列向量、负边界和非法输入。随机部分使用独立双重循环作为参考,不调用 histcounts、discretize 或 accumarray。贡献采用小整数,因此本组累计不涉及大幅消去或溢出,可以用精确相等检查,避免让宽松容差掩盖分错箱的问题。
matlab
function test_bins(fun)
if nargin==0, fun=@bin_totals; end
% Mutations caught: excluding final edge, right-closed interior, empty=zero.
x=[-1 0 .25 1 1.5 2 3 4]; w=[99 2 4 6 -2 0 10 99]; edges=[0 1 2 3];
[n,s,m,b]=fun(x,w,edges);
assert(isequal(b,[0 1 1 2 2 3 3 0]),'test:endpoints','Endpoint contract violated');
assert(isequal(n,[2 2 2]) && isequal(s,[6 4 10]) && isequal(m,[3 2 5]),'test:aggregate','Count/sum/mean incorrect');
[n,s,m,b]=fun([0 3],[0 8],edges);
assert(isequal(n,[1 0 1]) && isequal(s,[0 0 8]) && isequaln(m,[0 NaN 8]),'test:emptyBin','Empty bin is not zero-valued bin');
[n,s,m,b]=fun([],[],edges);
assert(isequal(n,[0 0 0]) && isequal(s,[0 0 0]) && all(isnan(m)) && isempty(b),'test:emptyInput','Empty data failed');
[n,s,m,b]=fun([0;1;2;3],[1;2;3;4],edges);
assert(isequal(size(b),[4 1]) && isequal(b,[1;2;3;3]) && isequal(s,[1 2 7]),'test:column','Column shape failed');
[n,s,m,b]=fun([-2 -1 0 1],[1 2 3 4],[-2 -1 0 1 2]);
assert(isequal(b,[1 2 3 4]) && isequal(n,[1 1 1 1]),'test:negativeEdges','Negative edges failed');
[n,s,m,b]=fun([1-eps/2 1 1+eps 2],[1 1 1 1],[0 1 2]);
assert(isequal(b,[1 2 2 2]) && isequal(n,[1 3]),'test:neighbors','Boundary neighbors failed');
bad={ [0 NaN],[1 1],[0 1],'bins:Data'; [0 Inf],[1 1],[0 1],'bins:Data'; ...
[0 1],[1 NaN],[0 1],'bins:Data'; [0 1],[1 Inf],[0 1],'bins:Data'; ...
[0 1],1,[0 1],'bins:Size'; [0 1],[1 2],[0 0 1],'bins:Edges'; ...
[0 1],[1 2],[1 0],'bins:Edges'; [0 1],[1 2],[0 Inf],'bins:Edges'; ...
[0 1],[1 2],0,'bins:Edges'; single(0),1,[0 1],'bins:Data'; ...
0,single(1),[0 1],'bins:Data'; 0,1,single([0 1]),'bins:Edges'; ...
[0 1i],[1 1],[0 1],'bins:Data'; sparse([0 1]),[1 1],[0 1],'bins:Data'; ...
ones(2),ones(2),[0 1],'bins:Data'; [0 0],[realmax realmax],[0 1],'bins:Overflow'};
for k=1:size(bad,1)
caught=false;
try, fun(bad{k,1},bad{k,2},bad{k,3}); catch e, caught=strcmp(e.identifier,bad{k,4}); end
assert(caught,'test:invalid','Invalid bin input did not raise specified error');
end
rng(20261112,'twister');
for k=1:1000
edges=[-10 -3 0 2 10];
x=[randi([-24 24],1,100)/2 edges]; w=randi([-10 10],size(x));
[n,s,m,b]=fun(x,w,edges);
rn=zeros(1,4); rs=rn; rb=zeros(size(x));
% Independent nested-loop oracle, no histcounts/discretize/accumarray.
for j=1:numel(x)
for q=1:4
if x(j)>=edges(q) && (x(j)<edges(q+1) || (q==4 && x(j)==edges(end)))
rb(j)=q; rn(q)=rn(q)+1; rs(q)=rs(q)+w(j); break
end
end
end
rm=rs./rn;
assert(isequal(b,rb) && isequal(n,rn) && isequal(s,rs) && isequaln(m,rm),'test:random','Independent bin oracle mismatch');
end
fprintf('BINS PASS: 6 deterministic, 16 invalid, 1000 x 105 oracle samples; integer aggregates exact\n');
[n,s,m,b]=fun([0 3],[0 8],[0 1 2 3]);
fprintf('EMPTY-BIN: count=%s sum=%s mean=%s\n',mat2str(n),mat2str(s),mat2str(m));
end
在两个文件所在目录运行:
text
matlab -wait -batch "test_bins" -logfile bins.log
2026-11-11 在 Windows MATLAB R2025b(25.2.0.2998904)原生执行,退出码为 0,日志为:
text
BINS PASS: 6 deterministic, 16 invalid, 1000 x 105 oracle samples; integer aggregates exact
EMPTY-BIN: count=[1 0 1] sum=[0 0 8] mean=[0 NaN 8]
每轮随机数据含 100 个位置和 5 个精确边界,因此不仅随机碰运气,还强制包含所有边界。独立参考逐点检查所属区间,验证箱号、计数、累计和与平均值全部一致。
5. 故意漏掉最后端点,测试会怎样
保存下面的错误版本为 bad_bins_last.m。它故意把等于最右端点的样本设成 bin=0,仅用于展示端点契约的价值。
matlab
function [n,s,m,b]=bad_bins_last(x,w,edges)
[~,~,b]=histcounts(x,edges); b(x==edges(end))=0;
k=numel(edges)-1; in=b>0;
n=accumarray(b(in).',1,[k 1]).';
s=accumarray(b(in).',w(in).',[k 1]).'; m=s./n;
end
运行:
text
matlab -wait -batch "test_bins(@bad_bins_last)" -logfile bad_last.log
本次原生运行在首个断言失败,提示 Endpoint contract violated,进程退出码为 1。这说明测试真的检查最后端点是否保留,而不只是看看函数能否执行。另一个已实测突变是把正式函数中的 meanValue=NaN(1,k) 改成全零初始化;它会被空箱断言拒绝。检查的是统计语义,不是变量名或某一行文本是否存在。
6. 浮点和数据解释的边界
边界判断针对已经存入 double 的实际值。如果业务上的"1"来自前面若干次浮点运算,它可能成为略大于或略小于 1 的数,从而落入不同区间。本文不会自动加一个 eps 修补所有边界,因为这会悄悄移动分箱线,尤其不适合跨度差异很大的边界。若业务需要量化到固定刻度,应明确量化单位并在分箱前处理。
贡献累计仍采用普通 double 求和。正负大数相消时会损失有效数字,求和顺序也可能影响末位;有限输入并不保证高精度结果,检测到非有限结果也不代表所有有限结果都足够准确。对于严重消去、长序列或财务级精确累计,应另行选择补偿求和、整数单位或高精度方案,并针对这些需求重新验证。
最后,越界样本没有消失:bin=0 就是可追踪的标记,可以通过 nnz(bin==0) 检查未纳入统计的记录数。先区分"非法输入""合法但越界""箱子为空"和"箱内贡献为零",再决定业务上如何处理,比给所有特殊情况统一补零更可靠。