【问题标题】:How to properly round-up half float numbers?如何正确舍入半浮点数?
【发布时间】:2015-10-08 15:11:43
【问题描述】:

我正面临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,但结果是一样的。

【问题讨论】:

  • 无法解释 round() 的行为,但如果您总是想四舍五入,可以使用 math.ceil()
  • @yurib 我希望将1.3 向下舍入为1,所以我不能使用ceil()
  • 自从我学习错误分析以来已经过去了很多天。但是,如果我没记错的话,5*10**-k 的四舍五入取决于它前面的数字。通过向上取整偶数位数和向下取整偶数位数,一半时间得到正错误,一半时间得到偶数错误(理论上)。当您执行许多添加时,这些错误可以相互抵消

标签: python python-3.x floating-point rounding precision


【解决方案1】:

Numeric Types section 明确记录了这种行为:

round(x[, n])
x 四舍五入到 n 位,四舍五入到偶数。如果 n 省略,则默认为 0。

注意四舍五入到偶数。这也称为银行家四舍五入;而不是总是向上或向下舍入(复合舍入误差),通过舍入到最接近的 偶数 数来平均舍入误差。

如果您需要对舍入行为进行更多控制,请使用decimal module,它可以让您准确指定rounding strategy should be used 的内容。

例如,从一半向上取整:

>>> 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

【讨论】:

  • IEEE 754 四舍五入到偶数也在 en.wikipedia.org/wiki/Rounding#Round_half_to_even 中描述
  • 在您的示例中,与仅使用 rounding 参数相比,修改本地上下文是否有好处:n.to_integral_value(rounding=ROUND_HALF_UP)
  • @dhobbs:设置上下文一次在意图上更清晰,但从技术角度来看没有区别。
【解决方案2】:

例如:

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)

【讨论】:

    【解决方案3】:

    你可以用这个:

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

    它会正确地向上或向下舍入数字。

    【讨论】:

    • 一次又一次让我震惊的是没有这样的内部功能。我的意思是,这不像现在人们在 python 中使用大量的 numpy 和数学来实现数值算法......
    • 它确实会“向上或向下”四舍五入,但不幸的是不适用于负数。
    【解决方案4】:

    round() 将向上或向下舍入,具体取决于数字是偶数还是奇数。一个简单的四舍五入方法是:

    int(num + 0.5)
    

    如果您希望它对负数正常工作,请使用:

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

    请注意,这可能会导致大数字或 5000000000000001.00.49999999999999994 等非常精确的数字变得混乱。

    【讨论】:

    • 此解决方案未解决一些细微问题。例如,如果num = -2.4,这会给出什么结果? num = 0.49999999999999994 呢? num = 5000000000000001.0?在使用 IEEE 754 格式和语义的典型机器上,此解决方案对所有这三种情况都给出了错误的答案。
    • @Mark Dickinson 我已经更新了帖子以提及这一点。谢谢
    • 严格来说,0.49999999999999994 和 5000000000000001.0 都存在精度问题。在这两种情况下,添加 0.5 会导致必要的精度位“下降”IEEE 754 双(64 位)尾数(52 小数位 + 隐式 1.0)的右侧。第一种情况基本上将值加倍,将(设置)LSB 推出,而第二种情况大到 0.5 小于现有 LSB 值。事实上,对于 2^52 = 2^53 加上 0.5 什么都不做。
    【解决方案5】:

    您看到的行为是典型的 IEEE 754 舍入行为。如果它必须在与输入相同的两个数字之间进行选择,它总是选择偶数。这种行为的优点是平均舍入效果为零 - 相同数量的数字向上和向下舍入。如果您以一致的方向四舍五入数字,则四舍五入会影响预期值。

    如果目标是公平舍入,那么您看到的行为是正确的,但这并不总是需要的。

    获得所需舍入类型的一个技巧是加上 0.5,然后发言。例如,将 0.5 与 2.5 相加得到 3,底楼为 3。

    【讨论】:

      【解决方案6】:

      喜欢fedor2612 的答案。我为那些想要使用这个函数来四舍五入任何小数位数的人扩展了一个可选的“小数”参数(例如,如果你想将货币从 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

      【讨论】:

      • 很棒的功能!从来没有想过我会遇到这样的问题,将 133.125 用 2 位数字舍入到 133.13 或 133.12 。谢谢,伙计!
      【解决方案7】:

      短版:使用decimal module。它可以精确地表示像 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 都是选项。

      【讨论】:

        【解决方案8】:

        为什么要这么复杂?

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

        你当然可以把它变成一个 lambda,它是:

        HalfRoundUp = lambda value: int(value + 0.5)
        

        【讨论】:

        • 不会为其他答案增加任何价值。
        • @RocketNikita Epic.
        【解决方案9】:

        四舍五入到最接近的偶数已成为数值学科的常见做法。 “四舍五入”会稍微偏向较大的结果。

        所以,从科学机构的角度来看,round 的行为是正确的。

        【讨论】:

        • 当然,如果您正在处理测量数据、运行模拟等,这是正确的方法。但它是不正确的,例如,如果您想在学校计算成绩。在匈牙利,我们有一个 5 级的评分系统,平均 4.5 被四舍五入到 5。我在教儿子 Python 时用这个作为例子,当 round(4.5) 给 5 时我惊呆了。我有一个很难向他解释为什么 Python 舍入与他在学校所学的舍入不同......
        【解决方案10】:

        这是另一种解决方案。 它将在 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) -&gt; 0.2, c_round(0.5, 0) -&gt; 1

        【讨论】:

        【解决方案11】:

        以下解决方案在不使用 decimal 模块的情况下实现了“学校时尚四舍五入”(结果很慢)。

        def school_round(a_in,n_in):
        ''' python uses "banking round; while this round 0.05 up" '''
            if (a_in * 10 ** (n_in + 1)) % 10 == 5:
                return round(a_in + 1 / 10 ** (n_in + 1), n_in)
            else:
                return round(a_in, n_in)
        

        例如

        print(round(0.005,2)) # 0
        print(school_round(0.005,2)) #0.01
        

        【讨论】:

          【解决方案12】:

          在这个问题中,这基本上是一个正整数除以 2 时的问题。最简单的方法是 int(n + 0.5) 用于单个数字。

          但是我们不能将其应用于系列,因此我们可以为例如 pandas 数据帧做的事情,而不进入循环,是:

          import numpy as np
          df['rounded_division'] = np.where(df['some_integer'] % 2 == 0, round(df['some_integer']/2,0), round((df['some_integer']+1)/2,0))
          

          【讨论】:

            【解决方案13】:

            所以为了确保这里有一个清晰的工作示例,我编写了一个小的便利函数

            def round_half_up(x: float, num_decimals: int) -> float:
                """Use explicit ROUND HALF UP. See references, for an explanation.
            
                This is the proper way to round, as taught in school.
            
                Args:
                    x:
                    num_decimals:
            
                Returns:
                        https://stackoverflow.com/questions/33019698/how-to-properly-round-up-half-float-numbers-in-python
            
                """
            
                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
            

            以及一组合适的测试用例

            def test_round_half_up():
                x = 1.5
                y = round_half_up(x, 0)
                assert y == 2.0
            
                y = round_half_up(x, 1)
                assert y == 1.5
            
                x = 1.25
                y = round_half_up(x, 1)
                assert y == 1.3
            
                y = round_half_up(x, 2)
                assert y == 1.25
            
            

            【讨论】:

              【解决方案14】:

              在某些情况下,一些解决方案可能无法按预期工作。

              例如使用上面的函数:

              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
              

              【讨论】:

                【解决方案15】:

                你可以使用:

                from decimal import Decimal, ROUND_HALF_UP
                
                for i in range(1, 15, 2):
                    n = i / 2
                    print(n, "=>", Decimal(str(n)).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
                

                【讨论】:

                  【解决方案16】:

                  没有任何库的经典数学舍入

                  def rd(x,y=0):
                  ''' A classical mathematical rounding by Voznica '''
                  m = int('1'+'0'*y) # multiplier - how many positions to the right
                  q = x*m # shift to the right by multiplier
                  c = int(q) # new number
                  i = int( (q-c)*10 ) # indicator number on the right
                  if i >= 5:
                      c += 1
                  return c/m
                  
                  Compare:
                  
                  print( round(0.49), round(0.51), round(0.5), round(1.5), round(2.5), round(0.15,1))  # 0  1  0  2  2  0.1
                  
                  print( rd(0.49), rd(0.51), rd(0.5), rd(1.5), rd(2.5), rd(0.15,1))  # 0  1  1  2  3  0.2
                  

                  【讨论】:

                    【解决方案17】:

                    知道round(9.99,0) 轮到int=10int(9.99) 轮到int=9 带来成功:

                    目标:根据value提供越来越低的整数

                        def get_half_round_numers(self, value):
                            """
                            Returns dict with upper_half_rn and lower_half_rn
                            :param value:
                            :return:
                            """
                            hrns = {}
                            if not isinstance(value, float):
                                print("Error>Input is not a float. None return.")
                                return None
                    
                            value = round(value,2)
                            whole = int(value) # Rounds 9.99 to 9
                            remainder = (value - whole) * 100
                    
                            if remainder >= 51:
                                hrns['upper_half_rn'] = round(round(value,0),2)  # Rounds 9.99 to 10
                                hrns['lower_half_rn'] = round(round(value,0) - 0.5,2)
                            else:
                                hrns['lower_half_rn'] = round(int(value),2)
                                hrns['upper_half_rn'] = round(int(value) + 0.5,2)
                    
                            return hrns
                    

                    一些测试:

                    是的

                    【讨论】:

                      【解决方案18】:
                      import math
                      # round tossing n digits from the end
                      def my_round(n, toss=1):
                      
                          def normal_round(n):
                              if isinstance(n, int):
                                  return n
                              intn, dec = str(n).split(".")
                              if int(dec[-1]) >= 5:
                                  if len(dec) == 1:
                                      return math.ceil(n)
                                  else:
                                      return float(intn + "." + str(int(dec[:-1]) + 1))
                              else:
                                  return float(intn + "." + dec[:-1])
                      
                          while toss >= 1:
                              n = normal_round(n)
                              toss -= 1
                          return n
                      
                      
                      for n in [1.25, 7.3576, 30.56]:
                          print(my_round(n, 2))
                      
                      1.0
                      7.36
                      31
                      

                      【讨论】:

                        【解决方案19】:

                        你可以试试这个

                        def round(num):
                            return round(num + 10**(-9))
                        

                        它将起作用,因为num = x.5 将始终是x.5 + 0.00...01,在它更接近x+1 的过程中,因此round 函数将正常工作并将x.5 舍入到x+1

                        【讨论】:

                        • 现在x.499999999 将进行半偶数舍入,并且(有一半时间,假设浮点精度问题不会以某种方式强制它)会被四舍五入。这比最初的情况更糟糕,因为您现在正在四舍五入到更远的数字。
                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2013-07-08
                        • 1970-01-01
                        • 2013-07-16
                        • 2012-12-28
                        • 2018-08-13
                        • 1970-01-01
                        相关资源
                        最近更新 更多