为什么 awk 不使总和为零而是一个非常小的数字?

为什么 awk 不使总和为零而是一个非常小的数字?

我有这个文件,我想对第一列中的所有数字求和。简单的:

awk '{s+=$1;print $1,s}' file
0.1048 -1.2705
0.4196 -0.8509
0.4196 -0.4313
0.2719 -0.1594
0.0797 -0.0797
0.0797 -5.55112e-17   #Notice this line

你看,最后一个应该是 0。我知道它e-17是零,但有时输出恰好是 0。如果它不是 0,则输出在e-15to范围内e-17,以负号或正号表示。为了解决这个问题,我必须使用绝对值:

awk '{s+=$1;if (sqrt(s^2)<0.01) s=0;print $1,s}' file

你知道为什么会发生这种情况吗?

答案1

发生这种情况是因为计算机在处理数字时的精度有限。并且可用精度使用二进制格式来表示数字。

这使得在我们的十进制系统中看似微不足道的数字只能表示为近似值(请参阅维基百科条目对此):例如0.1(如1/10)实际上存储为类似于0.100000001490116119384765625计算机上的内容。

所以你所有的号码实际上只由一个处理近似(除非你很幸运并且有这​​样的数字0.5这​​样的数字可以表示确切地)。

将所有这些近似数字相加最终可能会导致错误!= 0

答案2

作为解决此问题的方法,您可以使用专门设计用于处理算术运算的程序,例如bc

$ awk '{printf "%s + ",$1}' file | sed 's/\+ $/\n/' | bc
0

如果(似乎是这种情况)您有固定的小数位数,您可以简单地删除它们以处理整数,然后在末尾再次添加它们:

$ awk '{sub("0.","",$1);s+=$1;}END{print s/10000}' file
0

或者

$ perl -lne 's/0\.//; $s+=$_; END{print $s/10000}' file
0

答案3

大多数版本awk都有一个printf命令。代替

print $1,s

使用

printf "%.4f %.4f\n",$1,s

输出将四舍五入到小数点后 4 位。这样您就不会看到大多数舍入错误。

答案4

你的问题是“为什么会发生这种情况?”,但你隐含的问题(其他人已经解决了)是“我该如何解决这个问题?”您找到了一种方法,您在评论中提出了这种方法:

那么如果我把它乘以1000来消除这个点,我就能得到准确的结果,不是吗?

是的。嗯,10000,因为有四位小数。考虑一下:

awk '{ s+=$1*10000; print $1, s/10000 }'

不幸的是,这不起作用,因为一旦我们将令牌(字符串)解释为十进制数,损坏就已经发生了。例如,printf "%.20f\n"显示输入数据0.4157 实际上被解释为 0.41570000000000001394。在本例中,乘以 10000 得到的结果是:4157。但是,例如0.5973= 0.59730000000000005311,乘以 10000 得到 5973.00000000000090949470。

所以我们尝试

awk '{ s+=int($1*10000); print $1, s/10000 }'

将“应该是”整数的数字(例如,5973.00000000000090949470)转换为相应的整数(5973)。但这会失败,因为有时转换误差为负;例如,0.7130是 0.71299999999999996714。 Andawk的函数会截断(朝向零)而不是四舍五入,7129 也是如此。int(expr)int(7129.99999999)

所以,当生活给你柠檬时,你就制作柠檬水。当工具为您提供截断函数时,您可以添加 0.5 进行舍入。 7129.99999999+0.5≈7130.49999999,当然int(7130.49999999)是7130。但请记住:int()截断趋向于零,并且您的输入包含负数。如果要将 –7129.99999999 舍入为 –7130,则需要减去0.5 得到 –7130.49999999。所以,

awk '{ s+=int($1*10000+($1>0?0.5:-0.5)); print $1, s/10000 }'

$1*10000如果$1is ≤ 0,则添加 –0.5 。

相关内容