天天看點

從“四舍五入”到“奇進偶舍”

處理取整時,大概下意識的可以想到的方法,都是“四舍五入”吧?不過我們可以先看兩個例子,在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
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%)。雖然計算的規則稍微複雜一些,但是“奇進偶舍”這種舍入方法的優勢還是非常明顯的,這也是這種方法成為推薦标準,也被越來越多的程式設計語言和資料庫把這種方法作為預設實作的原因。