鱼C论坛

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

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

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

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

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

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

但这种方案,如果 num = 6.654
结果就变成了
>>> num = 6.654
>>> num = (num * 100 + 0.5) / 100
>>> num
6.659
运算结果是直接把数值结果都给更改了,这算解决了吗?并没有

再比如:
【学习笔记】四舍五入的方法,比小甲鱼的有效多了!
(出处: 鱼C论坛)一文中的解决方案是
看,结果和你想象的不一样吧!
要做到真正的四舍五入,应该这样:
def new_round(x, dight=0):
        temp_dight = 0.5 * (0.1 ** dight)
        temp_number = x * (10 ** dight)
        number = int(temp_number + temp_dight)
        return number / (10 ** dight)
>>> new_round(5.99)
6.0
>>> new_round(4.22, 1)
4.2
>>> new_round(5.5)
6.0
>>>
看似没有问题,于是我试了试new_round(2.67,1)
>>> new_round(2.67,1)
2.6
好家伙,这是怎么回事?2.65没四舍五入成功还好说,2.67都没成功还真有点说不过去了
仔细一推敲完全就是错误的函数(0.5完全没能成功的加在末位上啊)
于是我把它修改了下:
>>> def new_round(x, dight=0):
    temp_dight = 0.5 * (0.1 ** dight)
    temp_number = x * (10 ** dight)
    number = int((x+ temp_dight)*(10**dight))/(10**dight)
    return number
>>> new_round(2.66564,3)
2.666
>>> new_round(2.67,2)
2.67
>>> new_round(2.67,1)
2.7
>>> new_round(0.5,1)
0.5
>>> new_round(0.5,0)
1.0
>>> new_round(2.66544,3)
2.665

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

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

另一篇python3:小数位的四舍五入(用两种方法解决round 遇5不进)中有这么一种算法:
from _pydecimal import Decimal, Context, ROUND_HALF_UP
print(Context(prec=3, rounding=ROUND_HALF_UP).create_decimal('1.325'))
打印结果:
1.33
【算法二】为什么要prec=3,这种很难控制的量作为函数值呢,于是我加上我自己的理解把上述算法修改成如下:
def new_round_dec(_float=float, _len=2):
    '''
    对输入的_float做四舍五入运算,并输出结果
    args:
        _float:需要四舍五入的数字,为浮点数
        _len:保留小数位数,默认=2,为整形
    -----------------
    return:
        返回Decimal格式的结果
    '''
    if str(_float).find('.') == -1:
        return _float
    else:
        # prec= (str(_float).find('.')+_len),整数部分加+小数点+保留位数=从左至右保留字符串长度
        #rounding=ROUND_HALF_UP  取舍方式采取四舍五入方式
        #.create_decimal(str(_float))  创建一个decimal格式的值
        return Context(prec=(str(_float).find('.')+_len), rounding=ROUND_HALF_UP).create_decimal(str(_float))
>>> from decimal import *
>>> new_round_dec(1.1235654,4)
Decimal('1.1236')
>>> new_round_dec(1.1235654,5)
Decimal('1.12357')
>>> new_round_dec(1313.1235654,6)
Decimal('1313.123565')
>>> new_round_dec(1313.1235654,7)
Decimal('1313.1235654')

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



想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2021-1-27 11:37:45 | 显示全部楼层
真是学习了。
想知道小甲鱼最近在做啥?请访问 -> 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)
想知道小甲鱼最近在做啥?请访问 -> 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 )
一句解决,学习了~
想知道小甲鱼最近在做啥?请访问 -> 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)

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

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

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

好全面。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-1-16 15:41

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

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