从“四舍五入”到“奇进偶舍”

处理取整时,大概下意识的可以想到的方法,都是“四舍五入”吧?不过我们可以先看两个例子,在Python 3中,round(4.5) == 4,而在mongodb 以上的版本中,{$round: 4.5}的结果也是4。对于习惯了只存在“四舍五入”这一种舍入方法的同学们来说,估计是要去怀疑这是不是代码的bug了。其实,这里舍入的方法并不是“四舍五入”,而是采用了所谓的“奇进偶舍”或者“四舍六入五成双”的方法,这种方法也被称为Banker's Rounding(银行家舍入法)。Python 3选择了这种舍入方法作为标准库的实现,最主要的原因还是因为这个舍入方法被IEEE 754标准选为了默认的浮点数舍入方法和Decimal的推荐默认舍入方法(Round to nearest, ties to even)

作为默认舍入方法被推荐,并且还有Banker's Rounding这么一个拉风的名字,这个方法的优势在什么地方呢?首先,以舍入到整数为例,让我们来看一下,“奇进偶舍”这个方法的规则是什么。这里,我们就从Round to nearest, ties to even这个定义来解释。首先是Round to nearest,就是向最接近的整数来舍入,比如5.6最接近的整数是6,而-3.2最接近的整数是-3,前面举得几个例子其实和“四舍五入”的规则是完全一样的,不同之处在于,当小数部分正好是0.5时,那么这个数到两边的整数的距离是完全一样的,这时ties to even这后半条规则就要派上用场了,也就是当到两边整数的距离相等时,向最接近的偶数舍入,比如0.5舍入到0,4.5舍入到4,而1.5则要舍入到2。

从上面的规则可以看出来,广为人知的“四舍五入”的规则还是要简单很多的,但是“四舍五入”这种方法会引入一个比较容易积累误差的问题。还是舍入到整数为例,当小数部分恰好是最中间的0.5时,这个部分总是向上取整的,于是向上取整的可能性就会比向下取整多,那么得到舍入之后的数字就会倾向于偏大,尤其是在类似于在计算比如收入数据之类只需要保留一两位小数这些情形中,这个误差就很容易提现出来。而进一步的,如果对已经舍入过的数字进行求和等计算,这个误差会被积累和放大,经过多级的数据统计之后,一些最终统计报表上的结果就会与实际数字差的很远。采用“奇进偶舍”这种方法时,如果小数部分恰好是0.5,舍入时会以均等的概率向上或者向下取整,所以舍入之后偏大或者偏小的倾向也会相互抵消,从而在概率上让实际的误差趋向于0。

下面我们设计一个实验来对比两种舍入方法的误差积累。我们可以使用Python的decimal模块来完成这个实验,在decimal模块的Decimal.to_integral_value函数中,可以指定rounding参数为decimal.ROUND_HALF_UP或者decimal.ROUND_HALF_EVEN在两种舍入方法中进行选择。主要的测试程序如下:

import decimal
import math
import random


def get_random_decimal(n, f):
    '''
    生成decimal.Decimal随机数,整数n位,小数f位
    '''
    return decimal.Decimal(int(random.random() * 10 ** (n + f))) / decimal.Decimal(10 ** f)

def test(n, f, count=1000):
    '''
    进行求和测试并计算舍入的误差,count为随机数的个数,整数n位,小数f位
    '''
    sum_float = decimal.Decimal(0.0)
    sum_round_half_up = decimal.Decimal(0.0)
    sum_round_half_even = decimal.Decimal(0.0)
    for i in range(count):
        v = get_random_decimal(n, f)
        sum_float += v
        sum_round_half_up += v.to_integral_value(rounding=decimal.ROUND_HALF_UP)
        sum_round_half_even += v.to_integral_value(rounding=decimal.ROUND_HALF_EVEN)
    error_round_half_up = (sum_float - sum_round_half_up).copy_abs()
    error_round_half_even = (sum_float - sum_round_half_even).copy_abs()
    rate_round_half_up = '%.4f%%' % (error_round_half_up / sum_float * 100)
    rate_round_half_even = '%.4f%%' % (error_round_half_even / sum_float * 100)
    return [count, sum_float,
            sum_round_half_up, error_round_half_up, rate_round_half_up,
            sum_round_half_even, error_round_half_even, rate_round_half_even]

# 范例调用方法
# test(2, 2, count=10000)

我们把随机数值控制在100以内,并且保留两位小数(n==2, f==2),在不同的count下可以得到如下的结果

count sum_float sum_up error_up rate_up sum_even error_even rate_even
0 10000 502250 502336 86.33 0.0172% 502279 29.33 0.0058%
1 100000 5.00007e+06 5000671 604.66 0.0121% 5000208 141.66 0.0028%
2 1000000 5.00414e+07 50046394 5008.05 0.0100% 50041434 48.05 0.0001%
3 10000000 5.00122e+08 500170946 48962.4 0.0098% 500120975 1008.57 0.0002%

从上面的结果中(以_up结尾的为"四舍五入"的,以_even结尾的为"奇进偶舍"的)可以看出,“奇进偶舍”的误差是明显小于“四舍五入”的,而且会随着count的增大而越来越趋于0(“四舍五入”在这个设定下会趋于0.01%)。虽然计算的规则稍微复杂一些,但是“奇进偶舍”这种舍入方法的优势还是非常明显的,这也是这种方法成为推荐标准,也被越来越多的编程语言和数据库把这种方法作为默认实现的原因。

上一篇:调整css让语雀在更宽的屏幕上展示和编辑


下一篇:使用oss c sdk自定义上传和下载callback