如何正确地四舍五入半精度浮点数?

126

我正在遇到round()函数的一种奇怪行为:

for i in range(1, 15, 2):
    n = i / 2
    print(n, "=>", round(n))

这段代码输出:

0.5 => 0
1.5 => 2
2.5 => 2
3.5 => 4
4.5 => 4
5.5 => 6
6.5 => 6

我原本期望浮点数总是向上取整,但实际上它们被舍入到最接近的偶数。

为什么会出现这种行为,有什么方法可以获得正确的结果?

我尝试使用fractions,但结果仍然相同。


2
无法解释 round() 的行为,但如果您始终想向上舍入,可以使用 math.ceil() - yurib
8
我希望将 1.3 四舍五入为 1,因此不能使用 ceil() - Delgan
1
可能是限制浮点数为两位小数的重复问题。 - yurib
3
自我学习误差分析以来已经过去了很多天。但是如果我没记错的话,对于 5*10**-k 的四舍五入取决于它前面的数字。通过将奇数数字向上舍入,偶数数字向下舍入,理论上你会得到一半时间正误差和一半时间偶误差。当进行许多加法运算时,这些误差可以互相抵消。 - StoryTeller - Unslander Monica
24个回答

105

Numeric Types部分,明确记录了这种行为:

round(x[, n])
x四舍五入到n位小数,采用“银行家舍入”方式。如果省略n,则默认为0。

请注意“银行家舍入”。这也称为“银行家舍入法”;通过将数字四舍五入至最接近的偶数,而不总是向上或向下舍入(避免累积舍入误差),您可以平均四舍五入误差。

如果需要更多精确地控制舍入行为,请使用decimal模块,该模块允许指定正好使用哪种舍入策略。

例如,要从一半的数字向上舍入:

>>> from decimal import localcontext, Decimal, ROUND_HALF_UP
>>> with localcontext() as ctx:
...     ctx.rounding = ROUND_HALF_UP
...     for i in range(1, 15, 2):
...         n = Decimal(i) / 2
...         print(n, '=>', n.to_integral_value())
...
0.5 => 1
1.5 => 2
2.5 => 3
3.5 => 4
4.5 => 5
5.5 => 6
6.5 => 7

2
IEEE 754舍入到偶数也在https://en.wikipedia.org/wiki/Rounding#Round_half_to_even中描述。 - Robert E
1
在您的示例中,修改本地上下文是否比仅使用 rounding 参数更有优势,例如:n.to_integral_value(rounding=ROUND_HALF_UP) - dhobbs
1
@dhobbs:在意图上,只设置一次上下文更清晰明确,但从技术角度来看并没有区别。 - Martijn Pieters

56
例如:
from decimal import Decimal, ROUND_HALF_UP

Decimal(1.5).quantize(0, ROUND_HALF_UP)

# This also works for rounding to the integer part:
Decimal(1.5).to_integral_value(rounding=ROUND_HALF_UP)

35

你可以使用这个:

import math
def normal_round(n):
    if n - math.floor(n) < 0.5:
        return math.floor(n)
    return math.ceil(n)

它将适当地将数字四舍五入。


2
每每我都感到惊讶,为什么没有这样的内部函数。我的意思是,现在人们在Python中使用大量的NumPy和数学库来实现数值算法... - lalala
1
它可以将数字四舍五入,但不幸的是无法处理负数。 - drws

29

round()函数会根据数字的奇偶性进行四舍五入。只想向上取整的一个简单方法是:

int(num + 0.5)
如果您希望此功能也能适用于负数,请使用以下方法:

((num > 0) - (num < 0)) * int(abs(num) + 0.5)

注意,对于大数字或非常精确的数字例如5000000000000001.00.49999999999999994,这可能会出现问题。


3
这个解决方案没有解决一些微妙的问题。例如,如果 num = -2.4,这个解决方案会给出什么结果?如果 num = 0.49999999999999994num = 5000000000000001.0 呢?在使用 IEEE 754 格式和语义的典型计算机上,这个解决方案对这三种情况都给出了错误的答案。 - Mark Dickinson
@Mark Dickinson 我已经更新了帖子并提到了这一点。谢谢。 - Matthew D. Scholefield
严格来说,由于精度问题,0.49999999999999994和5000000000000001.0都存在问题。在这两种情况下,添加0.5会导致必要的精度位从IEEE 754双精度(64位)尾数(52个分数位+隐式1.0)的右侧“掉落”。第一种情况基本上是将值加倍,将(设置的)LSB推出,而第二种情况则太大,以至于0.5小于现有的LSB值。实际上,对于2 ^ 52 <= num < 2 ^ 53,它将把整数舍入为偶数(可能是由于数学芯片将80位内部舍入到64位输出)。num> = 2 ^ 53添加0.5不起作用。 - Uber Kluger

13

为什么要搞得这么复杂?(仅适用于正数

def HalfRoundUp(value):
    return int(value + 0.5)

当然,你可以将它转换为一个lambda函数,如下所示:
HalfRoundUp = lambda value: int(value + 0.5)

不幸的是,这个简单的答案不能处理负数,但可以通过数学中的floor函数进行修正:(这也适用于正数和负数

from math import floor
def HalfRoundUp(value):
    return floor(value + 0.5)

7
对其他答案没有任何增值。 - Rocket Nikita
这个页面上最佳答案!真不敢相信Python没有内置解决方案。 - simomo

12

喜欢fedor2612的回答。我用一个可选的“decimals”参数扩展了它,以便那些想要使用此函数四舍五入任意数量小数的人使用(例如,如果您想将货币 $26.455 四舍五入为 $26.46)。

import math

def normal_round(n, decimals=0):
    expoN = n * 10 ** decimals
    if abs(expoN) - abs(math.floor(expoN)) < 0.5:
        return math.floor(expoN) / 10 ** decimals
    return math.ceil(expoN) / 10 ** decimals

oldRounding = round(26.455,2)
newRounding = normal_round(26.455,2)

print(oldRounding)
print(newRounding)

输出:

26.45

26.46


太好了!从未想过要对保留2位小数的133.125进行四舍五入时会出现这样的问题,无法得到133.13或133.12。谢谢,伙计! - akushyn

11
您所看到的行为是典型的IEEE 754舍入行为。如果选择两个与输入等距离的数字,它总是选择偶数。这种行为的优点是平均舍入效果为零——同样数量的数字向上和向下舍入。如果您以一致的方向舍入半路数字,则舍入将影响预期值。
如果目标是公平舍入,则您所看到的行为是正确的,但并非总是所需的。
获得所需的舍入类型的一个技巧是添加0.5,然后取底数。例如,将0.5加到2.5中得到3,向下取整为3。

3

简单来说,使用decimal模块。它可以精确表示像2.675这样的数字,而不像Python中的浮点数,其中2.675实际上是真的2.67499999999999982236431605997495353221893310546875(确切地说)。并且您可以指定所需的舍入方式:ROUND_CEILING、ROUND_DOWN、ROUND_FLOOR、ROUND_HALF_DOWN、ROUND_HALF_EVEN、ROUND_HALF_UP、ROUND_UP和ROUND_05UP都是选项。


2

由于某些解决方案中的四舍五入有可能在某些情况下无法按预期工作,因此需要进行小幅修改。

例如,使用上述函数:

from decimal import Decimal, ROUND_HALF_UP
def round_half_up(x: float, num_decimals: int) -> float:
    if num_decimals < 0:
        raise ValueError("Num decimals needs to be at least 0.")
    target_precision = "1." + "0" * num_decimals
    rounded_x = float(Decimal(x).quantize(Decimal(target_precision), ROUND_HALF_UP))
    return rounded_x
round_half_up(1.35, 1)
1.4
round_half_up(4.35, 1)
4.3

我原本期望的是4.4,但对我有用的是先将x转换为字符串。

from decimal import Decimal, ROUND_HALF_UP
def round_half_up(x: float, num_decimals: int) -> float:
    if num_decimals < 0:
        raise ValueError("Num decimals needs to be at least 0.")
    target_precision = "1." + "0" * num_decimals
    rounded_x = float(Decimal(str(x)).quantize(Decimal(target_precision), ROUND_HALF_UP))
    return rounded_x

round_half_up(4.35, 1)
4.4

2

这里有另一种解决方案。它将像Excel中的普通四舍五入一样工作。

from decimal import Decimal, getcontext, ROUND_HALF_UP

round_context = getcontext()
round_context.rounding = ROUND_HALF_UP

def c_round(x, digits, precision=5):
    tmp = round(Decimal(x), precision)
    return float(tmp.__round__(digits))

c_round(0.15, 1) -> 0.2, c_round(0.5, 0) -> 1

该段文字为编程相关内容,意思是对0.15保留1位小数进行四舍五入得到0.2,对0.5保留0位小数进行四舍五入得到1。其中的代码部分需要保留HTML格式不做解释。

1
有任何疑问关于 tmp.round 的,请参考 Python 内置函数源代码 - round() 或者 Python 3 Decimal rounding half down with ROUND_HALF_UP context - Uber Kluger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接