python 确保浮点数计算结果正确的方法

因为计算机采用二进制,无法精确的表达浮点数

python 中 用 8字节64位存储空间分配了52位来存储浮点数的有效数字,11位存储指数,1位存储正负号

以下是4字节32为存储的模型

二进制的表示值表达浮点数是(-1)^sign × (1+0.Mantissa) × 2^(Expoment-127)其中127是单精度浮点数的偏移量

而Mantissa最多保留23位,所以在进行累加运算,如果二者指数位相差过大,将会导致数值较小的一方超过23位的尾数全部丢失,造成误差, 52位同理

另一方面,由于有的数据需要保留位数,细微的误差可能导致进位错误

例如:

c = 0.005
a = 0
for i in range(20):
    a += c
    f = float('%.2f' % a)
    if f == a or i % 2 == 1:
        pass
    else:
        print(f, a)

"""
结果
0.01 0.005
0.01 0.015
0.03 0.025
0.04 0.035
0.04 0.045
0.05 0.05499999999999999
0.06 0.06499999999999999
0.07 0.075
0.09 0.085
0.1 0.09500000000000001
"""

可以看到当循环执行第3次的时候,运算结果为0.0149999999999999, 此时真实值应为0.015

保留两位应为0.02, 但是由于小数点后第三位为4, 所以输出结果为0.01, 尽管浮点数产生的误差可以忽略不记,但保留小数点的操作,导致误差被放大到无法忽略的程度,尤其是在财务方面,对数据敏感的行业中,这种误差是不可忽略的

如何确保数据正确呢?

而如果要保证数据的正确性,一般采用两种方法:

方法一: 采用四舍六入五成双的原则

当不需要保留浮点数后面的位数时,可以采用round()

在python3 中,ruand()函数实际上采用的是,值被舍入到接近10的负ndigits次幂的倍数,如果与两个倍数的距离相等,则选择偶数,那么round取整的情况下,就会采用四舍六入五成双的原则

print(round(2.5))
print(round(3.5))
print(round(4.5))
print(round(5.5))
print(round(6.5))

"""
2
4
4
6
6
"""

运行结果永远是偶数

但是在保留小数点的情况下,round并不实行这一原则

print(round(2.115, 2))
print(round(2.125, 2))
print(round(2.135, 2))
print(round(2.145, 2))
print(round(2.155, 2))

"""
2.12
2.12
2.13
2.15
2.15
"""

.

方法二:四舍五入的原则

通过模型可知, 如果浮点数不保留小数点,那么只要判断Mantissa首位是否为1,如果是1则进1.

但是如果要保留小数点后未知位数就比较麻烦

一个可行的办法是使用rounding + decimal的方法

import decimal

decimal.getcontext().rounding = "ROUND_HALF_UP"
for i in range(10):
    a = i + 0.005
    b = f'{i}.005'
    print(a, b)
    # 此方法下,输入的数值是浮点数,字符串均可 
    print(decimal.Decimal(a).quantize(decimal.Decimal("0.01")))
    print(decimal.Decimal(b).quantize(decimal.Decimal("0.01")))

"""
浮点数保留两位结果
0.01
1.00
2.00
3.00
4.00
5.00
6.00
7.00
8.01
9.01
0:00:00
"""
"""
字符串保留两位结果
0.01
1.01
2.01
3.01
4.01
5.01
6.01
7.01
8.01
9.01
"""


不过在这个方法下,如果进行浮点数运算,再传入数据,因为运算导致偏差叠加,可能会导致结果偏离预期, 所以此方法不能确保进行计算的数据的精确性.

到此,我自己已知的库没有很好的能解决我的问题

三.解决方法

那么,我们能不能自己写个方法,来确保运算精度呢?

我的解决方法思路是这样的

浮点数保留位数的误差来自于二进制不能完美的表达十进制小数部分,但是二进制可以完美的转化十进制整数,那么能不能将十进制的小数部分转化为整数来处理呢?

已知大部分语言,小数部分都是有限位的,那么理论上,我们可以把浮点数差分成小数点前面和后面两个整数部分,将两个整数部分分别计算,最后合并,就可以获得想要的值

理论可行,于是,我得到了以下的解决方法

def add_base(bit, n1, n2):
    # 由于传入的数据可能不是浮点数,所以转变参数类型
    n1, n2 = float(n1), float(n2)
    # 两个数据按照符号可分为两种情况,符号相同或符号不同
    if (n1 >= 0 and n2 >= 0) or (n1 <= 0 and n2 <= 0):
        num = same_sb(n1, n2, bit)
    else:
        num = different_sb(n1, n2, bit) if n1 > 0 else different_sb(n2, n1)
    num = float(num)
    # 最终返回浮点数,并保留bit位
    return float('%.{}f'.format(bit) % num)


def split_float(num):
    # 将浮点数转为字符串,分割整数部分和小数部分,并返回int格式的整数和字符串格式的小数部分
    num = str(num).split('.')
    integer = num[0]
    try:
        fraction = num[1]
    except IndexError:
        fraction = '0'
    return [int(integer), fraction]


def same_sb(n1, n2, bit):
    n1 = split_float(n1)
    n2 = split_float(n2)
    # 获取两组数据中,小数部分最长的长度
    long = len(n1[1]) if len(n1[1]) > len(n2[1]) else len(n2[1])
    long = bit + 1 if long < bit +1 else long
    # 空位补零
    n1[1] += '0' * (long - len(n1[1]))
    n2[1] += '0' * (long - len(n2[1]))
    fraction = str(int(n1[1]) + int(n2[1]))
    integer = n1[0] + n2[0]
    # 如果计算后位数增加,说明计算结果进位,如果不足,说明前几位为0, 需要转为字符串后首位补零
    if long < len(fraction):
        integer += 1 if integer > 0 else -1
        fraction = fraction[-long:]
    elif long > len(fraction):
        fraction = '0' * (long - len(str(fraction))) + str(fraction)
    fraction_copy = fraction[0: bit - 2]
    fraction_change = fraction[bit - 1]
    fraction_change = f'{int(fraction_change) + 1}' if int(fraction[bit]) >= 5 else fraction_change
    fraction = fraction_copy + fraction_change
    return f'{integer}.{fraction}'


def different_sb(n1, n2, bit):
    flag = True if n1 > -n2 else False
    n1 = split_float(n1)
    n2 = split_float(n2)
    long = len(n1[1]) if len(n1[1]) > len(n2[1]) else len(n2[1])
    n1[1] += '0' * (long - len(n1[1]))
    n2[1] += '0' * (long - len(n2[1]))
    fraction = int(n1[1]) - int(n2[1]) if flag else int(n2[1]) - int(n1[1])
    if fraction < 0:
        fraction += int('1'+'0'*long)
        n1[0] += -1 if flag else 1
    fraction = '0' * (long - len(str(fraction))) + str(fraction)
    integer = str(n1[0] + n2[0])
    fraction_copy = fraction[0: bit - 2]
    fraction_change = fraction[bit - 1]
    fraction_change = f'{int(fraction_change) + 1}' if int(fraction[bit]) >= 5 else fraction_change
    fraction = fraction_copy + fraction_change
    return f'{integer}.{fraction}'


def test():
    a = ['两个数符号相同情况没有进位', '两个数符号相同情况有进位', '两个数不同且正数绝对值比较大的情况', '两个数不同且正数绝对值比较小的情况',
         '两个数不同且正数绝对值比较大的情况', '两个数不同且正数绝对值比较小的情况', '保留位数大于给出样例的情况']
    b = [(1.2654, 1.6816), (-1.5464, -3.6854), (1.2354, -3.5645), (3.5465, -1.68446), (-1.2646, 5.35462), (-2.6561, -1.265), (1.1, 1.2)]
    c = ['2.95', '-5.23', '-2.33', '1.86', '4.09', '-3.92', '-2.3']
    for i in range(7):
        print(f'{a[i]}\n 计算获得值为: {add_base(2, *b[i])}, 应当获得值为: {c[i]}')

    print(add_base(2, 24081.067, -27753), 24081.067 - 27753)
    print(add_base(2, 9385.95, -27753), 9385.95 - 27753)
    print(add_base(2, 24081.067, -3671.33), 24081.067 - 3671.33)
    print(add_base(2, 0, 24081.067), 24081.067)

test()

最终结果完美的解决了我对浮点数保留位数的需求

我们对运算速度进行一次测试

decimal.getcontext().rounding = "ROUND_HALF_UP"

t1 = datetime.datetime.now()
for i in range(1000):
    a = i + 0.005
    b = i + 0.005
    float('%.2f' % a)
    float('%.2f' % b)
t2 = datetime.datetime.now()
print('float', t2 - t1)

t1 = datetime.datetime.now()
for i in range(1000):
    a = i + 0.005
    b = i + 0.005
    round(a, 2)
    round(a, 2)
t2 = datetime.datetime.now()
print('round', t2 - t1)


t1 = datetime.datetime.now()
for i in range(1000):
    a = i + 0.005
    b = f'{i}.005'
    decimal.Decimal(a).quantize(decimal.Decimal("0.01"))
    decimal.Decimal(b).quantize(decimal.Decimal("0.01"))
t2 = datetime.datetime.now()
print('decimal', t2 - t1)

t1 = datetime.datetime.now()
for i in range(1000):
    add_base(2, i, 0.005)
    add_base(2, i, '0.005')
t2 = datetime.datetime.now()
print('my_del', t2 - t1)

float 0:00:00.000998

round 0:00:00.000997
decimal 0:00:00.002992
my_del 0:00:00.009973

结果处理2000组数据,我们的方法用时最长,用时0.01s,

所以如果数据精度不敏感还是使用float()或者round()最快,

如果浮点数直接传递,不参与计算,可以使用rounding + decimal,

如果数据量不是非常大,且要求精度很高,则使用次方法

你可能感兴趣的:(python)