四舍五入的八种策略

四舍五入的影响有多大?
四舍五入的错误曾左右了选举,甚至导致了生命的损失。

如何对数字进行四舍五入是很重要的,作为一个负责任的开发者和软件设计师,你需要知道常见的问题是什么以及如何处理这些问题。让我们深入研究一下不同的四舍五入方法是什么,以及如何在纯Python中实现每一种方法。

假设你有一个非常幸运的一天,发现地上有100元钱。与其一次花光所有的钱,你决定精打细算,通过购买一些不同的股票来投资你的钱。

股票的价值取决于供求关系。想买股票的人越多,该股票的价值就越大,反之亦然。在高成交量的股票市场上,某只股票的价值会以秒为单位进行波动。

我们来做个小实验。我们假设你所购买的股票的整体价值每秒钟都会有一些小的随机数波动,比如说在0.05美元和-0.05美元之间。这个波动不一定是一个只有两位小数的漂亮数值。例如,总价值可能前一秒增加0.031286美元,下一秒减少0.028476美元。

你不想把你的数值记录到小数点后第五或第六位,所以你决定砍掉小数点后第三位的所有数值。在四舍五入的行话中,这叫做将数字截断到小数点后第三位。这里会有一些误差,但通过保留三位小数,这个误差不会很大。对吧?

为了使用Python运行我们的实验,我们先写一个truncate()函数,将一个数字截断到小数点后三位。

truncate()函数的工作原理是首先将数字n的小数点向右移动三位,将n乘以1000。这个新数字的整数部分用int()表示。最后,将小数点向左移三位,将n除以1000。
接下来,让我们定义模拟的初始参数。你需要两个变量:一个用来跟踪模拟完成后股票的实际值,一个用来跟踪每一步截断到小数点后三位的股票值。
首先将这些变量初始化为100。

actual_value, truncated_value = 100, 100。

现在让我们运行模拟100万秒(约11.5天)。每一秒钟,用随机模块中的uniform()函数生成一个介于-0.05和0.05之间的随机值,然后更新实际值和截断值。

import random

random.seed(100)

for _ in range(1000000):
... randn = random.uniform(-0.05, 0.05)
... actual_value = actual_value + randn
... truncated_value = truncate(truncated_value + randn)
...

actual_value
96.45273913513529

truncated_value
0.239

仿真的主要部分发生在for循环中,它在0到999,999之间的数字范围(1000000)中循环。每一步从range()中得到的值都存储在变量_中,我们在这里使用这个变量是因为我们在循环中实际上不需要这个值。

在循环的每一步,使用random.randn()生成一个介于-0.05和0.05之间的新随机数,并分配给变量randn。通过将randn加到actual_value中计算出你的投资新值,通过将randn加到truncated_value中,然后用truncate()将这个值截断,计算出截断后的总数。

通过检查运行循环后的 actual_value 变量可以看出,你只损失了大约 3.55 美元。然而,如果你一直在看truncated_value,你会认为你几乎失去了所有的钱!你会发现,你的钱几乎都被保留小数的取整操作近清零了。

注意:在上面的例子中random.seed()函数是用来给伪随机数生成器播种的,这样你就可以重现这里的输出。

要了解更多关于Python中的随机性,请查看Real Python的《在Python中生成随机数据(指南)》。

暂且不考虑 round() 的表现并不完全如你所愿,让我们尝试重新运行模拟。这次我们将使用 round() 在每一步都取整到小数点后三位,然后再次使用 seed() 进行模拟,得到与之前相同的结果。

random.seed(100)
actual_value, rounded_value = 100, 100

for _ in range(1000000):
... randn = random.uniform(-0.05, 0.05)
... actual_value = actual_value + randn
... rounded_value = round(rounded_value + randn, 3)
...

actual_value
96.45273913513529

rounded_value
96.258

多么大的差别啊!
虽然看起来令人震惊,但这个确切的错误在20世纪80年代初引起了相当大的轰动,当时为记录温哥华证券交易所价值而设计的系统将总指数值截断到小数点后三位,而不是四舍五入。四舍五入的错误曾左右了选举,甚至导致了生命的损失。

round共有八种取整的策略
Truncation

Rounding Up

Rounding Down

Interlude: Rounding Bias

Rounding Half Up

Rounding Half Down

Rounding Half Away From Zero

Rounding Half To Even

ROUND DOWN的含义是,向0的方向舍入,取满足小数点后位数的需要后,最靠经0的那个数。从数轴上来看,就是向中心的0点靠近。( Round towards zero. )
在python中,int()函数对浮点数的取整,就是采用的ROUND DOWN方式:

>>> int(1.2)
1
>>> int(1.9)
1
>>> int(-1.2)
-1
>>> int(-1.9)
-1

Python 内置的 round() 函数使用了哪种四舍五入策略?
How to Round Half to Even 一文中提到:
round() 函数使用 了 "round half to even" 策略:

-2.535保留2位 : -2.54
-2.545保留2位 : -2.54
-2.575保留2位 : -2.58
-2.595保留2位 : -2.6

print('-2.535保留2位 :',round(-2.535,2))
-2.54

print('-2.545保留2位 :',round(-2.545,2))
-2.54

print('-2.595保留2位 :',round(-2.595,2))
#-2.6

"四舍五入到偶数 "的策略,四舍五入到最接近的偶数。例如:0.465取小数点后两位该如何运用策略?
首先,小数点向右移动3位,变为46.5,落在46-47之间。就近原则看离46.5最近的偶数是46,结果是0.46

ROUND HALF EVEN也被称为银行家舍入法,这种舍入方法的规则是:向离最近的偶数靠 Round to nearest with ties going to nearest even integer.

"四舍五入到偶数 "的策略的优点?
四舍五入到偶数 "策略没有基于数据集中纽带符号的偏差。如果数据集中有更多的纽带四舍五入到偶数,而不是四舍五入,它可能会引入一个偏差,但这种情况发生的概率通常很低。在这里介绍的所有选择中,"四舍五入到偶数 "的偏差最小。

python内置的round函数,采用的就是ROUND HALF EVEN舍入方法,round函数不是在做四舍五入:

在Python中对数字进行四舍五入,用 "四舍五入 "的策略将1.73四舍五入到小数点后一位是:
1.7
1.8

print(round(1.74,1))
1.7
print(round(1.75,1))
1.8
print(round(1.74))
2

首先,将小数点向右移动一位,得到17.3。
然后四舍五入到最接近的整数,也就是18.最后,将小数点后移一位,得到1.8。
最后,将小数点后移一位,得到1.8。

用 "四舍五入 "的策略将数值-2.961四舍五入到小数点后两位,则...
-2.97
-2.96
将小数点向右移动两位,得到-296.1

print(round(-2.961,2))
-2.96

print(round(-2.961,1))
-3.0

print(round(-2.961))
-3

然后四舍五入到最接近的整数,在本例中是-297,因为四舍五入总是将数字向左舍入(向负无穷大)最后,将小数点后移两位,得到-2.97。

print(round(-2.541,2))
-2.54
print(round(-2.545,2))
-2.54
print(round(-2.546,2))
-2.55

当一个数保留到小数点后3位时,以下哪项是真的?
正数被向下舍入,负数被向上舍入。

正数和负数都是四舍五入。

正数是四舍五入,负数是四舍五入。

正数和负数都是四舍五入的。

当你把一个正数保留时,你只需把四舍五入后的数字截断。
例如,将1.7365截断到小数点后三位,结果是1.736。结果与四舍五入到小数点后三位的结果是一样的。
另一方面,将-1.7365截断到三位小数,结果是-1.736,在数字线上是-1.7365的右边,因此与四舍五入到三位小数相同。

用 "离零点半圆 "策略将-0.045四舍五入到小数点后2位的值是...
-0.05
-0.04

当你 "四舍五入到零 "的时候,你把正数的和值向上舍,负数的向下舍。
将-0.045中的小数点向右移动两位小数,得到-4.5。这正好介于-4和-5之间,但由于我们是从零开始四舍五入的,所以我们四舍五入到-5。
最后,将小数点向左后移两位,得到 -0.05。

使用 "四舍五入 "策略将4.65四舍五入到小数点后一位的值是......。
4.7
4.6

首先,将小数点向右移动一位,得到46.5。这正好是46和47之间的一半。
根据 "四舍五入到偶数 "的策略,我们应该把46.5四舍五入到最接近的偶数,也就是46。将小数点向左后移一位,得到4.6。

ROUND FLOOR
floor是地板的意思,ROUND FLOOR的意思,就是向下舍入,下的方向,在数轴上是向着负无穷的方向 Round towards -Infinity

python的 // 符号,做的计算,就是ROUND FLOOR,如果有float参与//运算,结果就是float:

>>> 3//2
1
>>> 7//2
3
>>> 7//-2
-4
>>> -3//2
-2
>>> 7//2.1
3.0
>>> 7.1//2
3.0
>>> 7.1//2.1
3.0

ROUND CEILING
从这里开始,后续所有的舍入方法,都只在decimal模块中支持。
ROUND CEILING对应ROUND FLOOR,表示向上舍入,上就是无穷大的方向 Round towards Infinity

Decimal('1.234').quantize(Decimal('.00'), rounding=ROUND_CEILING)
Decimal('1.24')
Decimal('-1.234').quantize(Decimal('.00'), rounding=ROUND_CEILING)
Decimal('-1.23')

ROUND UP
ROUND UP对应ROUND DOWN,永远向着远离0的方向舍入Round away from zero.

Decimal('1.234').quantize(Decimal('.00'), rounding=ROUND_UP)
Decimal('1.24')
Decimal('-1.234').quantize(Decimal('.00'), rounding=ROUND_UP)
Decimal('-1.24')

ROUND HALF DOWN
就近舍入,如果与两边的数距离相等,向0方向舍入Round to nearest with ties going towards zero.

Decimal('1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_DOWN)
Decimal('1.23')
Decimal('-1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_DOWN)
Decimal('-1.23')

ROUND HALF UP
就近舍入,如果与两边的数距离相等,向远离0的方向舍入Round to nearest with ties going away from zero.

Decimal('1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_UP)
Decimal('1.24')
Decimal('-1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_UP)
Decimal('-1.24')

ROUND 05UP
Round away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise round towards zero. 如果rounding之后,最后的数字是0或5,就向UP方向(远离0的方向)舍入;否则,就像靠近0的方向舍入。

Decimal('1.204').quantize(Decimal('.00'), rounding=ROUND_05UP)

7.1//2
3.0
>>> 7.1//2.1
3.0
```

ROUND CEILING
从这里开始,后续所有的舍入方法,都只在decimal模块中支持。
ROUND CEILING对应ROUND FLOOR,表示向上舍入,上就是无穷大的方向 Round towards Infinity
>>> Decimal('1.234').quantize(Decimal('.00'), rounding=ROUND_CEILING)
Decimal('1.24')
>>> Decimal('-1.234').quantize(Decimal('.00'), rounding=ROUND_CEILING)
Decimal('-1.23')

ROUND UP
ROUND UP对应ROUND DOWN,永远向着远离0的方向舍入Round away from zero.
>>> Decimal('1.234').quantize(Decimal('.00'), rounding=ROUND_UP)
Decimal('1.24')
>>> Decimal('-1.234').quantize(Decimal('.00'), rounding=ROUND_UP)
Decimal('-1.24')


ROUND HALF DOWN
就近舍入,如果与两边的数距离相等,向0方向舍入Round to nearest with ties going towards zero.
>>> Decimal('1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_DOWN)
Decimal('1.23')
>>> Decimal('-1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_DOWN)
Decimal('-1.23')

ROUND HALF UP
就近舍入,如果与两边的数距离相等,向远离0的方向舍入Round to nearest with ties going away from zero.
>>> Decimal('1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_UP)
Decimal('1.24')
>>> Decimal('-1.235').quantize(Decimal('.00'), rounding=ROUND_HALF_UP)
Decimal('-1.24')

ROUND 05UP
Round away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise round towards zero. 如果rounding之后,最后的数字是0或5,就向UP方向(远离0的方向)舍入;否则,就像靠近0的方向舍入。

>>> Decimal('1.204').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('1.21')
Decimal('1.254').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('1.26')

Decimal('1.214').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('1.21')
Decimal('1.274').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('1.27')

Decimal('-1.204').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('-1.21')
Decimal('-1.254').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('-1.26')

Decimal('-1.214').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('-1.21')
Decimal('-1.274').quantize(Decimal('.00'), rounding=ROUND_05UP)
Decimal('-1.27')

以下哪种四舍五入策略最能减轻四舍五入的偏差?

四舍五入 "策略会将每个数字都向下舍入,所以这总是会引入一个向负无穷大的舍入偏差。
每当数据集中正数的数量等于负数的数量时,"截断 "策略就会相当好地处理偏差,但除此之外会引入一个偏差。
"舍半而上 "策略一般不会出现偏差,但只要数据集中有大量的平局,就会引入一轮向正无穷大的偏差。
"整半离零 "策略比 "整半向上 "能更好地缓解偏差,但如果数据集中所有的纽带都是正数,或者所有的纽带都是负数,就会引入一个偏差。

最后,"四舍五入到偶数 "策略没有基于数据集中纽带符号的偏差。如果数据集中有更多的纽带四舍五入到偶数,而不是四舍五入,它可能会引入一个偏差,但这种情况发生的概率通常很低。在这里介绍的所有选择中,"四舍五入到偶数 "的偏差最小。

为什么 round(-1.225, 2) 返回-1.23,而它应该返回-1.22?
浮点表示错误

-1.23
因为round()应该将平局四舍五入到最接近的偶数,在本例中是-1.22。
发生这种情况的原因与机器上存储浮点数的方式有关。

用来表示-1.22的二进制是一个无限重复的分数,它不能准确地存储在内存中。计算机将这个分数四舍五入到可以存储在内存中的最接近的二进制小数,这个小数比-1.225略小。所以内存中存储的-1.225这个数其实并不是平局,round(-1.225,2)将其四舍五入到-1.23`。

更详细的解释请参见《如何在 Python 中对数字进行四舍五入》一文中的旁白。

更详细的解释请参见 How to Round Half Down 一节中的旁白:How to Round Numbers in Python article。

假设你有下面的 Python 列表。
data = [0.15, -1.45, 3.65, -7.05, 2.45] 。

如果使用 "四舍五入 "策略将数据中的每一个数字四舍五入到小数点后一位,会产生以下哪种四舍五入偏差?

向正无穷大方向舍入的偏差

正确

趋向负无穷大的偏向

趋向零偏差

没有引入四舍五入的偏差

使用 "四舍五入 "策略,数据中的每一个数字都被四舍五入到小数点后一位,结果是以下列表。
[0.2, -1.4, 3.7, -7.0, 2.5]

新列表中的每一个数字都比数据中的对应数字大,所以引入了一个向正无穷大的四舍五入的偏向。

Flag
Rounding Strategy
decimal.ROUND_CEILING
Rounding up
decimal.ROUND_FLOOR
Rounding down
decimal.ROUND_DOWN
Truncation
decimal.ROUND_UP
Rounding away from zero
decimal.ROUND_HALF_UP
Rounding half away from zero
decimal.ROUND_HALF_DOWN
Rounding half towards zero
decimal.ROUND_HALF_EVEN
Rounding half to even
decimal.ROUND_05UP
Rounding up and rounding towards zero

编写Python函数round_half_towards_zero的代码,该函数接收一个数字n和一个默认为0的关键字参数小数,并返回n的值四舍五入到小数点后的数值,其中的平分是四舍五入到零。

你可以假设已经导入了数学模块,并且存在一个名为round_half_down()的函数,该函数接收两个参数--一个数字n和一个关键字参数decimals,并返回数字n四舍五入到小数点后的数值,其中的平分线向下舍入。

这就是我们期望看到的。

def round_half_towards_zero(n, decimals=0):
  rounded_abs = round_half_down(abs(n), decimals)
  return math.copysign(rounded_abs, n)

# Also acceptable:
def round_half_towards_zero(n, decimals=0):
  sign = 1 if n >= 0 else -1
  rounded_abs = round_half_down(abs(n), decimals)
  return sign * rounded_abs

# Without `round_half_down()`:
def round_half_towards_zero(n, decimals=0):
  sign = 1 if n >= 0 else -1
  multiplier = 10 ** decimals
  rounded_abs = math.ceil(abs(n)*multiplier - 0.5) / multiplier
  return sign * rounded_abs

-------------结束--------------

你可能感兴趣的:(四舍五入的八种策略)