鱼C论坛

 找回密码
 立即注册
查看: 2658|回复: 5

[技术交流] 关于精确四舍五入问题——欢迎探讨交流

[复制链接]
发表于 2021-1-26 06:06:59 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
最近看了很多关于round()函数带来的四舍五入不精确性的解决方案,最常见的就是乘以10的需要保留的小数位数次幂然后进行的一些列操作比如:
[已解决]python3怎么精确四舍五入保留两位小数?
(出处: 鱼C论坛)中
  1. >>> num = 6.655
  2. >>> num = (num * 100 + 0.5) / 100
  3. >>> num
  4. 6.66
复制代码


但这种方案,如果 num = 6.654
结果就变成了
  1. >>> num = 6.654
  2. >>> num = (num * 100 + 0.5) / 100
  3. >>> num
  4. 6.659
复制代码

运算结果是直接把数值结果都给更改了,这算解决了吗?并没有

再比如:
【学习笔记】四舍五入的方法,比小甲鱼的有效多了!
(出处: 鱼C论坛)一文中的解决方案是
看,结果和你想象的不一样吧!
要做到真正的四舍五入,应该这样:
  1. def new_round(x, dight=0):
  2.         temp_dight = 0.5 * (0.1 ** dight)
  3.         temp_number = x * (10 ** dight)
  4.         number = int(temp_number + temp_dight)
  5.         return number / (10 ** dight)
复制代码
  1. >>> new_round(5.99)
  2. 6.0
  3. >>> new_round(4.22, 1)
  4. 4.2
  5. >>> new_round(5.5)
  6. 6.0
  7. >>>
复制代码

看似没有问题,于是我试了试new_round(2.67,1)
  1. >>> new_round(2.67,1)
  2. 2.6
复制代码
好家伙,这是怎么回事?2.65没四舍五入成功还好说,2.67都没成功还真有点说不过去了
仔细一推敲完全就是错误的函数(0.5完全没能成功的加在末位上啊)
于是我把它修改了下:
  1. >>> def new_round(x, dight=0):
  2.     temp_dight = 0.5 * (0.1 ** dight)
  3.     temp_number = x * (10 ** dight)
  4.     number = int((x+ temp_dight)*(10**dight))/(10**dight)
  5.     return number
复制代码

  1. >>> new_round(2.66564,3)
  2. 2.666
  3. >>> new_round(2.67,2)
  4. 2.67
  5. >>> new_round(2.67,1)
  6. 2.7
  7. >>> new_round(0.5,1)
  8. 0.5
  9. >>> new_round(0.5,0)
  10. 1.0
  11. >>> new_round(2.66544,3)
  12. 2.665
复制代码


这下貌似对了但是仔细想想,这不还是小甲鱼老师教的末位加0.5吗?我只能说同学不细心,你还是没有小甲鱼老师厉害啊。。。。
可是这样就是正确的算法了吗?答案是。。。。还是不行的!
  1. >>> new_round(1.115,2)
  2. 1.11
复制代码
尴尬了。。。。难道就没有算法,把这个头痛的四舍五入通吃掉的吗?
———————————————————————————————这是分割线   ————————以下是和大家探讨的两个算法———————————————————————————————————————————
查阅了网上的资料,在一篇python 准确进行四舍五入的问题,头痛!
一文中,有这么个算法
  1. def new_round(_float, _len):
  2.     """
  3.     Parameters
  4.     ----------
  5.     _float: float
  6.     _len: int, 指定四舍五入需要保留的小数点后几位数为_len
  7.    
  8.     Returns
  9.     -------
  10.     type ==> float, 返回四舍五入后的值
  11.     """
  12.     if isinstance(_float, float):
  13.         if str(_float)[::-1].find('.') <= _len:
  14.             return(_float)
  15.         if str(_float)[-1] == '5':
  16.             return(round(float(str(_float)[:-1]+'6'), _len))
  17.         else:
  18.             return(round(_float, _len))
  19.     else:
  20.         return(round(_float, _len))
复制代码

这个算法中,我不太理解
if isinstance(_float, float):

【算法一】为什么要判断其是否为浮点型呢?,于是我加上我自己的理解把上述算法修改成如下:
  1. def new_round(_float=float(0),_lens=int(2)):
  2.     '''
  3.     对输入的_float做四舍五入运算,并输出结果
  4.     args:
  5.         _float:需要四舍五入的数字,为浮点数
  6.         _len:保留小数位数,默认=2,为整形
  7.     -----------------
  8.     return:
  9.         返回Decimal格式的结果
  10.     '''
  11.     if str(_float)[::-1].find('.') <= _lens:    #小数点后位数 比 保留小数位 小于或等于 时 直接四舍五入等于原值
  12.         _float = round(_float,_lens)
  13.         return _float
  14.     elif  str(_float)[str(_float)[:].find('.')+_lens+1] == '5': #小数点后位数 比 保留小数位 大 且保留小数位后1位为5时
  15.         y = str(_float)[:str(_float)[:].find('.')+_lens+1] + '6'+str(_float)[str(_float)[:].find('.')+_lens+2:] #将该位变成6
  16.         _float = round(float(y), _lens)
  17.         return _float
  18.     else:   #小数点后位数 比 保留小数位 大 且保留小数位后1位不为5时 直接四舍五入
  19.         _float = round(_float,_lens)
  20.         return _float
复制代码
  1. >>> new_round(1.115,2)
  2. 1.12
  3. >>> new_round(1.115,3)
  4. 1.115
  5. >>> new_round(1.1156,3)
  6. 1.116
  7. >>> new_round(1.1155,3)
  8. 1.116
  9. >>> new_round(0.5,0)
  10. 1.0
  11. >>>
复制代码
[b]
[/b]

另一篇python3:小数位的四舍五入(用两种方法解决round 遇5不进)中有这么一种算法:
  1. from _pydecimal import Decimal, Context, ROUND_HALF_UP
  2. print(Context(prec=3, rounding=ROUND_HALF_UP).create_decimal('1.325'))
  3. 打印结果:
  4. 1.33
复制代码

【算法二】为什么要prec=3,这种很难控制的量作为函数值呢,于是我加上我自己的理解把上述算法修改成如下:
  1. def new_round_dec(_float=float, _len=2):
  2.     '''
  3.     对输入的_float做四舍五入运算,并输出结果
  4.     args:
  5.         _float:需要四舍五入的数字,为浮点数
  6.         _len:保留小数位数,默认=2,为整形
  7.     -----------------
  8.     return:
  9.         返回Decimal格式的结果
  10.     '''
  11.     if str(_float).find('.') == -1:
  12.         return _float
  13.     else:
  14.         # prec= (str(_float).find('.')+_len),整数部分加+小数点+保留位数=从左至右保留字符串长度
  15.         #rounding=ROUND_HALF_UP  取舍方式采取四舍五入方式
  16.         #.create_decimal(str(_float))  创建一个decimal格式的值
  17.         return Context(prec=(str(_float).find('.')+_len), rounding=ROUND_HALF_UP).create_decimal(str(_float))
复制代码

  1. >>> from decimal import *
  2. >>> new_round_dec(1.1235654,4)
  3. Decimal('1.1236')
  4. >>> new_round_dec(1.1235654,5)
  5. Decimal('1.12357')
  6. >>> new_round_dec(1313.1235654,6)
  7. Decimal('1313.123565')
  8. >>> new_round_dec(1313.1235654,7)
  9. Decimal('1313.1235654')
复制代码


以上两种算法:
一是算法一中
if isinstance(_float, float):判断其是否为浮点型的必要性,我觉得似乎没有必要
二是这两种算法,有没有,没有想周全的地方和存在问题的地方
欢迎大家探讨,指出,谢谢!



小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2021-1-27 11:37:45 | 显示全部楼层
真是学习了。
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2021-1-27 11:50:58 | 显示全部楼层
本帖最后由 天下有雪 于 2021-1-27 11:54 编辑

官方文档说明,如果传入的参数为浮点数,并且这个浮点值在计算机里面不能被精确存储,那么它会先被转换为一个不精确的二进制值,然后再把这个不精确的二进制值转换为等效的十进制值。
对于不能精确表示的小数,当你传入的时候,Python在拿 到这个数前,这个数就已经被转成了一个不精确的数了。所以你虽然参数传入的是11.245, 但是Python拿到的实际上是11.244999999999...。
但是如果你传入的是字符串11.245’, 那么Python拿到它的时候,就能知道这是11.245,不会提前被转换为一个不精确的值,所以,建议给Decimal的第一个参数传入字符串型的浮点数,而不是直接写浮点数。
如果想实现精确的四舍五入,代码应该这样写:
from decimal import Decimal, ROUND_HALF_UP
origin_num = Decimal('11.245')
answer_num = origin_num. quantize(Decimal('0.00'),rounding=ROUND_HALF_UP )
print( answer_ num )

要做精确计算,那么就不应该再单独使用浮点数,而是应该总是使用 Decimal('浮点数')。否则,当你赋值的时候,精度已经被丢失了,建议全程使用Decimal,例如:
a = Decimal('0.1')
b = Decimal('0.2')
c=a+b
print(c)
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2021-1-27 19:03:46 | 显示全部楼层
天下有雪 发表于 2021-1-27 11:50
官方文档说明,如果传入的参数为浮点数,并且这个浮点值在计算机里面不能被精确存储,那么它会先被转换为一 ...

answer_num = origin_num. quantize(Decimal('0.00'),rounding=ROUND_HALF_UP )
一句解决,学习了~
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2021-1-27 19:12:20 | 显示全部楼层
本帖最后由 漫天疯语 于 2021-1-27 19:45 编辑

另,摘要搬运一篇:关于python decimal 精确计算


贴一个decimal文档里面的解释:

ROUND_CEILING (towards Infinity),
ROUND_DOWN (towards zero),
ROUND_FLOOR (towards -Infinity),
ROUND_HALF_DOWN (to nearest with ties going towards zero),
ROUND_HALF_EVEN (to nearest with ties going to nearest even integer),
ROUND_HALF_UP (to nearest with ties going away from zero), or
ROUND_UP (away from zero).
ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero)

举个例子
  1. from decimal import *
  2. x = Decimal('-3.333333333') + Decimal('-2.222222222')
  3. print(x)   # -5.555555555
  4. print(x.quantize(Decimal('1.0000'), ROUND_HALF_UP))              # -5.5556
  5. print(x.quantize(Decimal('1.0000'), ROUND_HALF_EVEN))          # -5.5556
  6. print(x.quantize(Decimal('1.0000'), ROUND_HALF_DOWN))        # -5.5556
  7. print(x.quantize(Decimal('1.0000'), ROUND_CEILING))               # -5.5555
  8. print(x.quantize(Decimal('1.0000'), ROUND_FLOOR))                 # -5.5556
  9. print(x.quantize(Decimal('1.0000'), ROUND_UP))                       # -5.5556
  10. print(x.quantize(Decimal('1.0000'), ROUND_DOWN))                 # -5.5555
复制代码
简单点
  1. from decimal import *
  2. x = Decimal('-0.5')
  3. print(x)   # -0.5
  4. print(x.quantize(Decimal('1'), ROUND_HALF_UP))         # -1    HALF_UP 即是我们所说的标准四舍五入
  5. print(x.quantize(Decimal('1'), ROUND_HALF_EVEN))     # -0    HALF_EVENT是quansize的默认值
  6. print(x.quantize(Decimal('1'), ROUND_HALF_DOWN))   # -0    HALF_DOWN同HALF_EVENT
  7. print(x.quantize(Decimal('1'), ROUND_CEILING))         # -0    CEILING倾向于正无穷
  8. print(x.quantize(Decimal('1'), ROUND_FLOOR))           # -1    FLOOR倾向于变得更小
  9. print(x.quantize(Decimal('1'), ROUND_UP))                 # -1    UP始终进位
  10. print(x.quantize(Decimal('1'), ROUND_DOWN))           # -0    DOWN始终不进位
复制代码

ROUND_HALF_EVENT 和 ROUND_HALF_DOWN:EVENT是quansize的默认设置值,可以通过getcontext()得到,EVENT四舍五入进了一位,DOWN为接近最近的0进了一位。
ROUND_CEILING 和 ROUND_FLOOR:CEILING超过5没有进位是因为它倾向正无穷,FLOOR为了总是变得更小所以进了一位。
ROUND_UP 和 ROUND_DOWN:UP始终进位,DOWN始终不会进位。

总结:
一共分三组
第一组    HALF_UP和HALF_EVENT(HALF_DOWN)
HALF_EVENT(HALF_DOWN)为quantize的默认值,它倾向于接近最近的0一方进位,存在对5是否进位会出现问题
HALF_UP为数学中标准的四舍五入
第二组    CEILING和FLOOR  他们的用法相对
CEILING倾向于正无穷,所以无论是否大于5,在正数中表现为进位,在负数中,表现为不进位
FLOOR倾向负无穷,所以无论是否大于5,在正数中表现为不进位,在负数中,他都会变得更小而进位。
第三组   UP和DOWN  他们的用法同样是相对的
UP始终进位
DOWN始终不进位。

小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2021-1-29 15:32:50 | 显示全部楼层
漫天疯语 发表于 2021-1-27 19:12
另,摘要搬运一篇:关于python decimal 精确计算

好全面。
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-6-28 11:37

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表