精度溢出问题
背景
python中定义好的浮点型数据,在实际业务系统传输过程中,出现了精度溢出的问题。具体实例如下:
加载数据
import numpy as np
import pandas as pd
#加载本地的测试数据
data_path=r'D:\desktop\data_b6da1bdd4fa54677a03994e0db9fb508.csv'
data=pd.read_csv(data_path)
data.head()
"""
time 366cf056-0f9b-11ed-9353-99e44f95bc32
0 0.0 0.066
1 0.2 0.052
2 0.5 0.023
3 0.8 0.006
4 1.0 0.004
"""
查看数据基础属性
data.info()#查看数据的基础信息
"""
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2049 entries, 0 to 2048
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 time 2049 non-null float64
1 366cf056-0f9b-11ed-9353-99e44f95bc32 2049 non-null float64
dtypes: float64(2)
memory usage: 32.1 KB
"""
业务应用:将dataframe转换成json
def dataframe_to_json(raw_data):
"""将dataframe转换成json的形式"""
data = raw_data.values.tolist()
cols = raw_data.columns.tolist()
output = list(map(lambda x: dict(zip(cols, x)), data))
return output
dataframe_to_json(data)[:10]#查看前10个
"""
[{'time': 0.0, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.066},
{'time': 0.2, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.052},
{'time': 0.5, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.023},
{'time': 0.8, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.006},
{'time': 1.0, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.004},
{'time': 1.2, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.009},
{'time': 1.5, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.003},
{'time': 1.8, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.002},
{'time': 2.0, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.002},
{'time': 2.2, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.003}]
"""
在jupyter或pycharm本地测试中显示没有任何问题,但是嵌入到开发的软件中,以exe的方式运行时,过程中偶尔传输的结果是:
#相同的python虚拟环境,相同的数据,在实际项目过程中运行的结果:
[{'time': 0.0, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.06599999964237213}, {'time': 0.2, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.052000001072883606}, {'time': 0.5, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.023000000044703484}, {'time': 0.8, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.006000000052154064}, {'time': 1.0, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.004000000189989805}, {'time': 1.2, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.008999999612569809}, {'time': 1.5, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.003000000026077032}, {'time': 1.8, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.0020000000949949026}, {'time': 2.0, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.0020000000949949026}, {'time': 2.2, '366cf056-0f9b-11ed-9353-99e44f95bc32': 0.003000000026077032}]
问题定位
与python中浮点数据的表示机制有关
相关文档说明
在stackflow(https://stackoverflow.com/questions/31977319/floating-point-precision-affected-when-converting-dataframe-to-list)上 我也查到了类似的问题,大家给到核心建议是使用"round"方法。
查看python官方文档关于《浮点算术:问题和限制》(https://docs.python.org/3.8/tutorial/floatingpoint.html)的相关说明:
核心说明
核心的说明有以下几点:
- 浮点数在计算机硬件中表示为以 2 为底的(二进制)分数。
- 不幸的是,大多数十进制分数不能完全表示为二进制分数。结果是,通常,您输入的十进制浮点数仅与实际存储在机器中的二进制浮点数近似。
- Python 只打印机器存储的二进制近似值的真实十进制值的十进制近似值。
- 有趣的是,有许多不同的十进制数共享相同的最接近的近似二进制分数。
- 请注意,这是二进制浮点的本质:这不是 Python 中的错误,也不是您的代码中的错误。您将在所有支持硬件浮点运算的语言中看到相同的内容(尽管某些语言默认或在所有输出模式下可能不会显示差异)。
- 表示错误问题:指某些(实际上是大多数)十进制分数不能完全表示为二进制(以 2 为底)分数的事实。这就是为什么 Python(或 Perl、C、C++、Java、Fortran 和许多其他语言)通常不会显示您期望的确切十进制数的主要原因。 这是为什么?1/10 不能完全表示为二进制分数。今天(2000 年 11 月)几乎所有机器都使用 IEEE-754 浮点运算,并且几乎所有平台都将 Python 浮点数映射到 IEEE-754 “双精度”。754 个双精度数包含 53 位精度,因此在输入时,计算机会努力将 0.1 转换为最接近的小数,其形式为J /2** N,其中J是正好包含 53 位的整数。
解决方案
- 使用format格式化输出(或f-string )
- 保存成分数的形式
#应用示意
format(math.pi, '.12g') #'3.14159265359'
#转化成分数
x=3.14159
x.as_integer_ratio()#转换成对应的分数表示
"""
(3537115888337719, 1125899906842624)#分子/分母
"""
x=0.25
x.as_integer_ratio()
"""
(1, 4)##分子/分母
"""
from decimal import Decimal
Decimal.from_float(0.1)
"""
Decimal('0.1000000000000000055511151231257827021181583404541015625')
"""
format(Decimal.from_float(0.1),".17")#'0.10000000000000001'
from fractions import Fraction
Fraction.from_float(0.1)
(0.1).as_integer_ratio()
"""
(3602879701896397, 36028797018963968)
"""
回归上述问题
但针对背景中提到的问题,我们可以看到输出的数据都是数值型,通过上述格式化方案或分数不足以解决问题。最红通过定位发现dataframe.values.tolist()
中才会出现该问题。
在dataframe结果时我们我们已经明确指定小数位数,但是通过dataframe.values.tolist()方法或者dataframe.values时会出现精度溢出的问题。 分析: 基于python中float型数据的保存机制,精度溢出是随机的,完全依赖于当前运行环境。 精度溢出以外的数据对我们的影响有两种情况:
第一种:通过保留小数(round),我们可以达到目标预期;
第二种:通过保留小数后的数值与我们的实际预期差异较大,这种可能性非常小。
解决方案: 本文给出一种结合业务场景的"round"方法,假定我们保存的结果数据可能有3位小数也可能包含6位小数,此时我们通过使用np.allclose()函数 比较误差,以此确定保留几位小数。以此减少精度溢出问题对我们的影响。
#代码优化形式
def dataframe_to_json(raw_data):
"""将dataframe转换成json的形式"""
data = raw_data.values # .tolist()
cols = raw_data.columns # .tolist()
try:
data1 = np.round(data, 3)
data2 = np.round(data, 6)
data = data1 if np.allclose(data1, data) else data2
except Exception:
pass
output = list(map(lambda x: dict(zip(cols, x)), data))
return output